/* styles/ui.css — tooltips, cards, modals, screens shared chrome */

:root {
  --ui-section-control-gap: 0.6rem;
  --ui-section-label-size: 1.2rem;
  --ui-section-letter-spacing: 0.08em;
  --ui-tray-side-margin: 0.4rem;
  /* The tray control is the same box as every close and ☰ (base.css). */
  --ui-tray-control-size: var(--iconbtn-size);
  --ui-tray-resize-surface: 44px;
  --ui-tray-expanded-block-max: 36rem;
  --ui-tray-expanded-panel-max: 34%;
}

/* Formation editor: a shared live grid beside a compact inspector. */
.modal:has([data-advanced-panel='Interface']:not([hidden]) [data-topic-panel='Formation layout']:not([hidden])) {
  width: min(120rem, calc(94vw / var(--ui-zoom, 1))); max-width: calc(94vw / var(--ui-zoom, 1));
}
.formation-editor { container-type: inline-size; width: 100%; min-width: 0; font-size: max(1.3rem, calc(14px / var(--ui-zoom, 1))); }
.formation-workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, .62fr); gap: 1.4rem; align-items: start; padding: .8rem .2rem; }
.formation-preview-panel, .formation-inspector { min-width: 0; }
.formation-preview-panel { border: 1px solid var(--line-soft); border-radius: var(--r-2); padding: 1rem; }
.formation-inspector { border-left: 1px solid var(--line); padding-left: 1.3rem; }
.formation-editor h3 { margin: 0 0 .8rem; font: 600 max(1.6rem, calc(18px / var(--ui-zoom, 1))) var(--font-ui, sans-serif); color: var(--parchment); }
.formation-editor h4 { margin: 0 0 .5rem; font: 600 max(1.3rem, calc(14px / var(--ui-zoom, 1))) var(--font-ui, sans-serif); color: var(--parchment); }
.formation-team-labels { display: grid; grid-template-columns: 1fr 1fr; text-align: center; gap: 1.5rem; margin: 1rem 0 .25rem; }
.formation-team-labels small { display: block; font-size: max(1.1rem, calc(12px / var(--ui-zoom, 1))); color: var(--parchment); opacity: .75; margin-top: .2rem; }
.formation-editor .formation-preview { display: block; position: relative; inset: auto; width: 100%; height: calc(42dvh / var(--ui-zoom, 1)); min-height: 260px; max-height: 480px; z-index: 0; }
.formation-preview .formation-grid-cell { transform: translate(-50%, -50%); pointer-events: none; }
.formation-preview .formation-grid-cell::after { display: none; }
.formation-preview .formation-grid-cell span { background: var(--bg-raise); color: var(--parchment); font-size: max(1.1rem, calc(12px / var(--ui-zoom, 1))); font-weight: 400; padding: .1rem .2rem; }
.formation-preview .formation-grid-outline > * { fill-opacity: .1; stroke-width: 1.3; }
.formation-dimensions { text-align: center; font-weight: 600; margin: 1rem 0 .5rem; }
.formation-preview-note, .formation-help { color: var(--parchment); opacity: .76; font-size: max(1.1rem, calc(12px / var(--ui-zoom, 1))); line-height: 1.45; margin: .5rem 0 .8rem; }
.formation-preview-note { text-align: center; }
[data-settings-host] .formation-editor .set-row { padding: .3rem 0; border: 0; min-width: 0; }
.formation-preset-field { display: grid; gap: .4rem; }
.formation-preset-field select { width: 100%; }
.formation-editor select, .formation-editor input { min-width: 0; max-width: 100%; font: inherit; color: var(--parchment); accent-color: var(--gold); }
.formation-editor select, .formation-editor input[type='number'] { background: var(--bg); border: 1px solid var(--line-soft); border-radius: var(--r-2); min-height: var(--tap-floor, 44px); padding: .4rem .6rem; }
.formation-presets { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .4rem; margin: .5rem 0 .8rem; }
.formation-presets button { display: flex; flex-direction: column; align-items: center; gap: .2rem; min-width: 0; min-height: 64px; padding: .35rem .15rem; font-size: max(1rem, calc(11px / var(--ui-zoom, 1))); background: var(--bg); border: 1px solid var(--line-soft); }
.formation-presets button[aria-pressed='true'] { border-color: var(--gold); background: var(--panel); color: var(--parchment); }
.formation-preset-diagram { width: 56px; max-width: 100%; height: 40px; fill: var(--gold); }
.formation-dimension-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .7rem; }
.formation-dimension-controls .formation-control { display: flex; flex-direction: column; align-items: stretch; gap: .4rem; }
.formation-control { display: grid; grid-template-columns: minmax(0, .8fr) minmax(0, 1.4fr); align-items: center; gap: .7rem; }
.formation-control label { font-size: inherit; }
.formation-inputs { display: flex; gap: .4rem; align-items: center; min-width: 0; }
.formation-inputs input[type='range'] { width: 100%; min-width: 30px; flex: 1; }
.formation-inputs input[type='number'] { width: 5.8rem; flex-shrink: 0; text-align: center; }
.formation-inputs > span { font-size: inherit; }
.formation-dimension-controls .formation-inputs input { width: 100%; }
.formation-control-section { border-top: 1px solid var(--line); padding-top: .9rem; margin-top: .8rem; }
.formation-visibility { display: flex; align-items: center; gap: .6rem; min-height: 44px; margin: .6rem 0; }
.formation-visibility input { width: 1.15rem; height: 1.15rem; }
.formation-adjustments { border: 1px solid var(--line-soft); border-radius: var(--r-2); margin-top: .7rem; }
.formation-adjustments summary { padding: .8rem; cursor: pointer; color: var(--parchment); }
.formation-adjustments summary span { display: inline-block; vertical-align: middle; }
.formation-adjustments summary small { display: block; font-size: max(1.1rem, calc(12px / var(--ui-zoom, 1))); opacity: .76; margin-top: .3rem; }
.formation-adjustment-fields { padding: 0 .8rem .7rem; }
.formation-adjustment-fields .formation-control { grid-template-columns: minmax(0, 1fr) auto; padding-block: .5rem; }
.formation-adjustment-fields .formation-control label { line-height: 1.35; }
.formation-apply-status { font-size: max(1.1rem, calc(12px / var(--ui-zoom, 1))); color: var(--parchment); line-height: 1.4; margin: .8rem 0; }
.formation-editor .formation-apply { width: 100%; background: var(--gold); color: var(--bg); min-height: 48px; font-weight: 700; }
.formation-editor .formation-apply:is(:hover, :focus-visible, :active) { background: var(--gold); color: var(--bg); border-color: var(--parchment); }
@container (max-width: 650px) {
  .formation-workspace { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
  .formation-inspector { border-left: 0; padding-left: 0; }
  .formation-editor .formation-preview { height: 300px; min-height: 290px; }
}
@container (min-width: 651px) {
  .formation-preview-panel { position: sticky; top: 1rem; }
}
@container (max-width: 370px) {
  .formation-preview-panel { padding: .6rem .3rem; }
  .formation-team-labels small { display: none; }
  .formation-control { grid-template-columns: minmax(0, 1fr); gap: .25rem; }
  .formation-editor .formation-preview { min-height: 340px; }
}
/* UI zoom can make a narrow viewport report a wide logical container. */
@media (max-width: 760px) {
  .formation-workspace { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
  .formation-preview-panel { position: static; }
  .formation-inspector { border-left: 0; padding-left: 0; }
  .formation-editor .formation-preview { height: calc(300px / var(--ui-zoom, 1)); min-height: calc(300px / var(--ui-zoom, 1)); }
}

/* ---------- card colour motif (Settings → Display → Card motif) ------------ */
/* Two independent axes, both player-chosen:
     class  → --card-tint, set per card from the class def's cardTint (data, so
              a new class brings its own hue with no edit here)
     accent → --gold / --accent-rgb, already themed by the accent setting
   The mode lives on :root[data-card-motif] and the wash depth on
   --card-motif-strength, so switching is a re-paint with no re-render.
   Colorless cards carry no --card-tint and keep the neutral frame. */
:root { --card-motif-strength: 0.10; }

/* wash — the class tints the card body; rarity keeps the border. */
:root[data-card-motif='wash'] .card[style*='--card-tint']::before {
  content: ''; position: absolute; inset: 0; border-radius: var(--r-3); pointer-events: none;
  background: var(--card-tint); opacity: var(--card-motif-strength);
}
/* (The banner used to take the accent here. The card TYPE owns it now — see
   the type block above — so the motif leaves it alone and the two systems
   stop fighting over the same element.) */

/* accent — your accent takes the border; rarity demotes to a corner pip so it
   stays readable, and the class shows as a large watermark sigil. */
:root[data-card-motif='accent'] .card { border-color: var(--gold); }
:root[data-card-motif='accent'] .card::after {
  content: ''; position: absolute; top: 5px; right: 5px; width: 9px; height: 9px;
  border-radius: var(--r-round); background: var(--line-soft); pointer-events: none;
}
:root[data-card-motif='accent'] .card.rarity-uncommon::after { background: var(--frost); }
:root[data-card-motif='accent'] .card.rarity-rare::after { background: var(--gold); }
:root[data-card-motif='accent'] .card.rarity-special::after { background: #6a3a7a; }
:root[data-card-motif='accent'] .card[style*='--card-tint']::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: var(--card-tint); opacity: calc(var(--card-motif-strength) * 1.3);
  -webkit-mask-image: radial-gradient(circle at 50% 52%, #000 38%, transparent 72%);
  mask-image: radial-gradient(circle at 50% 52%, #000 38%, transparent 72%);
}

/* band — a class stripe along the top and an accent rule under the art. */
:root[data-card-motif='band'] .card[style*='--card-tint']::before {
  content: ''; position: absolute; top: 0; left: 0; right: 0; height: 5px;
  background: var(--card-tint); pointer-events: none;
}
:root[data-card-motif='band'] .card .art { border-bottom: 2px solid var(--gold); }

.set-row-unavailable { opacity: 0.86; }
.set-action-status {
  max-width: 24rem; color: var(--muted); font-size: 1.05rem; line-height: 1.35; text-align: right;
}


/* ---------- keyboard / gamepad focus cursor ---------- */
/* The pad's cursor wears THE ring (--focus-ring, kit.css) — the same one a
   keyboard's :focus-visible wears — and nothing else: no second width, no
   glow. `!important` because a screen's hover rule may still say
   `outline: none` on the element the cursor is standing on. */
.gp-focus { outline: var(--focus-ring) !important; outline-offset: var(--focus-ring-offset); border-radius: var(--r-2); }
.map-node.gp-focus circle { stroke: var(--gold) !important; stroke-width: 4 !important; }

/* ---------- shared confirmation ---------- */
.confirmation-veil {
  z-index: 1200; box-sizing: border-box;
}
.confirmation-veil.confirmation-input-shield {
  padding: 0; background: transparent;
}

.farewell { gap: 2.2rem; }
.farewell .title-big { opacity: 0.9; }

/* ---------- generic screens ---------- */
/* A SCREEN THAT OVERFLOWS BRINGS ITS OWN SCROLLER, AND STOPS CENTRING WHEN IT DOES.
   Freja, 2026-08-07, EldenSpire#84. Constantine, playing: "upgrading cards, I
   notice I can't scroll on portrait mobile phone." Bjorn reproduced it at
   390x844 — 2402 px of Smith grid in an 844 px window, sixteen of twenty cards
   under the bottom edge, `scrollableAncestor: null`, a dragged finger moving
   nothing and the screenshots byte-identical.

   `html`/`body` are `overflow: hidden` BY DESIGN — this is a fullscreen game,
   not a document — so an overflowing screen has to carry its own scrollport.
   `.reward-row` carries one for x, `.armoury-body` for y; `.screen` carried
   none, and it is the container all sixteen mount into.

   TWO DECLARATIONS, NOT ONE, AND THE SECOND IS THE POINT. `overflow-y` alone
   makes this worse, measured: .screen is a flex item that today simply GROWS
   past the app box (844 -> 2159 local px), so the overflow is one-directional
   and the heading survives. Give it a scrollport and it stops growing — and
   then `justify-content: center` centres the overflow, and the start-side half
   of a centred overflow IS NOT SCROLLABLE. Measured on that one-line version:
   the player lands at scrollTop 0 with SHRINE OF EMBER at top -635.89, six
   cards already above the screen, and no gesture that can ever reach them.
   The last card becomes reachable and the first thing becomes unreachable.

   `safe center` is the whole fix for that: centred while it fits, start-aligned
   the moment it does not, decided per render by the browser rather than by a
   class anyone has to remember to add. The plain `center` before it is the
   fallback for engines without `safe` — one property, two values, later wins
   where understood; where it is not, screens are top-aligned, which is plain
   and never broken.

   WHY NOT `.deck-strip`: the strip is not what overflows. It wraps and grows,
   and the screen it grew inside is what ran out of room. Put the scroller on
   the grid and Shop's identical grid, the Rewards screen and the four other
   screens nobody has photographed all still need their own. One property, one
   container, sixteen screens.

   BOTH EDGES, measured at 390x844 and 1200x730: a screen that FITS is
   byte-identical before and after (the shrine before the grid opens, both
   shapes, same sha256) and gains no scrollbar; a screen that OVERFLOWS keeps
   every rect it had to three decimals — heading, first card, last card — and
   gains only the ability to reach the end. `.screen.customize` sets its own
   `overflow: hidden` and `flex-start` below and is unaffected. */
.screen {
  flex: 1; overflow-y: auto;
  display: flex; flex-direction: column; align-items: center;
  justify-content: center;
  justify-content: safe center;
  gap: 1.8rem; padding: 2.4rem;
  /* #29: when this container really travels, the edge says so in product
     colours rather than inheriting an OS-dependent thumb. `auto` still means
     no track at zero travel; the focused camera proves both endpoints. */
  scrollbar-width: thin; scrollbar-color: var(--line) transparent;
}
.screen::-webkit-scrollbar { width: 1.2rem; height: 1.2rem; }
.screen::-webkit-scrollbar-thumb {
  background: var(--line); border-radius: var(--r-2); border: 3px solid transparent;
  background-clip: padding-box;
}

/* The grace-refill sentence on the Shrine (Sten, 2026-08-08). NOT `.subtitle`:
   that rule's 0.3em tracking is right for a three-word banner and wrong for a
   sentence — at 390x844 it pushed the first draft of this line to six lines
   over the Rest button. Normal tracking, one measure, and it sits under the
   banner rather than pretending to be one.
   LAW 4: `font-size` in rem because it decides how big a letter is; the measure
   and the gaps are text-independent, so the box does not grow with Text size —
   and there is no fixed height, so the letters push it taller on their own. */
.screen .rest-refill {
  color: var(--gold);
  font-size: 0.95rem;
  line-height: 1.45;
  max-width: 34ch;
  margin: 0 auto 10px;
  text-align: center;
}

.flask-allocation-controls {
  display: flex; flex-wrap: wrap; gap: 6px;
}
.flask-allocation-controls button {
  min-width: var(--tap-floor); min-height: var(--tap-floor); height: auto;
  display: inline-flex; align-items: center; justify-content: center;
}
.coop-smithing-receipts b { color: var(--gold); }

/* reward screen */
.reward-row { display: flex; gap: 2.2rem; }

/* The card chooser's NEW badge belongs to the status stack above the card,
   never to the title band. The reward row owns the stack's shared dimensions
   in kit.css; fallbacks keep this component safe in an isolated render. The
   height scales with text, while the gap remains a small piece of box
   geometry. pointer-events: none — the badge marks the card, the CARD takes
   the tap. */
.card .card-badge-new {
  position: absolute;
  top: calc(-1 * (var(--reward-new-badge-height, calc(1.08rem + 6px)) + var(--reward-status-gap, 6px)));
  left: 50%; transform: translateX(-50%);
  box-sizing: border-box; height: var(--reward-new-badge-height, calc(1.08rem + 6px));
  display: inline-flex; align-items: center;
  font-size: 0.9rem; line-height: 1.2; font-weight: 700; letter-spacing: 0.08em;
  color: var(--gold); border: 1px solid var(--gold); border-radius: var(--r-3);
  padding: 2px 8px; background: rgba(20, 16, 10, 0.85);
  pointer-events: none; z-index: 2;
}

:root:not([data-layout='narrow']) .reward-menu .reward-kind:has(.reward-skip) {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 8px;
}
:root:not([data-layout='narrow']) .reward-kind:has(.reward-skip) .glyph { grid-column: 1 / -1; }
:root:not([data-layout='narrow']) .reward-kind:has(.reward-skip) .cp-body { grid-column: 1; grid-row: 2; }
:root:not([data-layout='narrow']) .reward-kind:has(.reward-skip) .reward-skip {
  grid-column: 2; grid-row: 2; position: relative; left: 2px;
}
/* SPEC §15.1: the card chance missed; one line where the card row would sit. */
.reward-menu .reward-note { flex-basis: 100%; margin: 0; color: var(--muted); font-style: italic; }

@keyframes reward-took {
  0% { border-color: var(--gold); transform: scale(1.04); }
  100% { border-color: var(--line-soft); transform: scale(1); }
}

/* EldenSpire#23 — Sunna's withhold. The reward screen is the first thing a
   portrait player meets after every fight, and it only became reachable at all
   because this branch fixed the fight. At 390x844 three cards need 417.6px in a
   390px viewport: the row overflowed 13.8px each side, the left card's cost
   badge showed 5.2px of 27, and — unlike every other bleed on this branch —
   THERE WAS NO SCROLLPORT, so no swipe recovered it.

   HER RULE WAS `overflow-x: auto`, AND ON ITS OWN IT CHANGES NOTHING. Measured:
   bleed 13.8px unmoved, travel still 0, badge still 5.2/27. `.reward-row` is a
   shrink-to-fit flex item inside `.screen`, which is `align-items: center` — so
   it sizes to its 417.6px of content, and a scroll container exactly as wide as
   its content has nothing to scroll. `.hand` got the same rule four lines away
   and worked because it is a grid item that stretches to its column. Same rule,
   different parent, opposite outcome.

   `max-width: 100%` is what lets the rule bite. With both: bleed 0 at all three
   portrait shapes, every cost badge fully visible, and 73-79px of real swipe
   travel where there was none. 1200x730 and 844x390 are untouched — the
   container query does not fire there, measured, 0 bleed and 0 travel both.

   LAW 5 RETIRED THAT SCROLLPORT ON THE PICK ROWS — Viki, 2026-08-14, on
   Marina's 2026-08-16 ruling (the coop reward rows' 73-79px is a defect,
   mine). Sunna's swipe was the right recovery before the law existed; clause 1
   now says horizontal travel on a narrow shape is ZERO, and clause 3 licenses
   the honest answer instead: a narrow shape is a DIFFERENT COMPOSITION. So a
   pick row WRAPS — three 14rem cards become rows of 2+1 and spend the vertical
   budget `.screen`'s own scrollport (#84, above) already owns. No card
   shrinks: `.card` keeps `flex-shrink: 0`, so clause 4's "fits by squeezing"
   trap cannot fire — measured, cards 126x176.4 local px before and after.

   `#shop-cards` USED TO KEEP THE OLD HORIZONTAL SCROLLPORT here while its
   composition was owned elsewhere. That dependency is resolved and the shelf
   wraps below. Keeping `overflow-x:auto` after the wrap made overflow-y compute
   to auto too and trapped the scaled cost tag 2px past that inner port's final
   endpoint at 360x640. The remaining `max-width` is the bound the wrap needs;
   the generic `.screen` above owns any honest vertical travel and its cue.

   THE SHOP'S OWN SHELVES ARE NO LONGER `.reward-row`: they are `.card-shelf`
   (kit.css), which wraps, centres and bounds itself at every width by the same
   rule that decides how many cards a row holds. The two `#shop-cards` rules
   that used to state the wrap here are gone rather than kept as a second
   answer; the rows below are the ones still dealt as `.reward-row` — the
   reward door, the draft and the co-op picks. */
  :root[data-layout='narrow'] .reward-row { flex-wrap: wrap; justify-content: center; max-width: 100%; }
.reward-row .card:hover { transform: translateY(-8px) scale(1.06); box-shadow: 0 10px 30px rgba(201, 162, 39, 0.25); }

/* THE MERCHANT'S SHELF WRAPS — Sten, 2026-08-14, on Marina's axisfit ruling
   (shop rows 397-403px, DEFECT, dated 2026-08-16). `#shop-cards` is a
   `.reward-row`, and the scrollport above was Sunna's fix for a THREE-card
   pick; the shop stocks `balance.shop.cardStock = 5`, so the same rule handed
   this row 788px of shelf in a 385px room — H 403px at 390x844, 397 at
   360x640, 316 at 884x1326, measured at 929b6ea by tools/axisfit.mjs, the
   only rows on the shop screen it fails.

   WRAP, NOT SCROLL, and Law 5 says which: clause 2 exempts a run only where
   the horizontal run IS the content, and a shelf you buy from is a grid that
   happens to be short — the remove grid TWO ELEMENTS DOWN already wraps
   (`.deck-strip`), so one screen was answering one question both ways. Clause
   3 licenses the rearrangement; clause 4 says spend the axis a thumb already
   scrolls — `.screen` owns overflow-y and the cards run off the BOTTOM.

   Count-agnostic on purpose (the class-card orphan ruling, above at
   `:root[data-layout='narrow'] .class-row`): cardStock is a table row
   Constantine can raise, and a layout correct only at today's count breaks
   the day content grows. Cards keep their authored size — nothing shrinks,
   tap floors hold (Law 4: the floor is ergonomic, not proportional).

   Scoped to #shop-cards, NOT to .reward-row: the coop reward rows are red in
   the same sweep and they are Viki's, dealt by name (wave-three board,
   2026-08-14) — a class-level wrap here would fix her rows behind her back.

   SUPERSEDED, 2026-09-20, and deliberately left standing as the reason. The
   wrap this rule asked for is now what `.card-shelf` does at every width, on
   every shop shelf rather than on the card row alone, so the rule itself is
   removed and its finding is not: a shelf that has to be told to wrap, once
   per id, is the shape the shelf primitive exists to end. */
.heal-note { color: var(--green); font-size: 1.3rem; }

/* ---------- save slots (title) ---------- */
.slot-list { display: flex; flex-direction: column; gap: 1rem; width: 43rem; max-width: 92%; animation: screenIn 500ms ease-out; }
.slot {
  display: flex; align-items: center; justify-content: space-between; gap: 1.2rem;
  padding: 1.1rem 1.6rem; border: 1.5px solid var(--line-soft); border-radius: var(--r-4);
  background: var(--panel); transition: border-color 150ms, transform 150ms;
}
.slot.occupied:hover { border-color: var(--gold); transform: translateY(-1px); }
.slot.empty { opacity: 0.9; }
.slot-info { display: flex; flex-direction: column; gap: 0.2rem; text-align: left; }
.slot-tag { color: var(--muted); font-size: 0.9rem; letter-spacing: 0.22em; }
.slot-title { color: var(--gold); font-family: var(--font-display); font-size: 1.5rem; }
.slot-meta { color: var(--parchment); font-size: 1.1rem; }
.slot-seed { color: var(--muted); font-size: 1rem; font-family: monospace; }
.slot-empty-label { color: var(--muted); font-size: 1.3rem; font-style: italic; }
.slot-actions { display: flex; align-items: center; gap: 0.8rem; flex-shrink: 0; }
.slot-delete { min-width: 3.4rem; padding: 0.6rem 0.8rem; }
/* The delete beat rides the shared hold machinery (secondbeat.js `deleteSave`,
   collapsed 2026-08-14 — the two-click `.armed` state died with it). While the
   hold is filling, the ✕ answers in the old arm's color: this is the one
   control where the fill means "a run is about to be gone for good", and the
   blood border is the same sentence the armed state used to say. Three class-
   grade selectors on purpose: `.beat-hold[data-hold='holding']` (further down)
   is (0,2,0) and later in the file, so an equal-specificity rule here would
   quietly lose to its gold. */
.slot-delete.beat-hold[data-hold='holding'] { color: var(--blood); border-color: var(--blood); }
/* THE WORD DOES NOT FIT ON AN ICON — the `.flask-slot .hold-hint` precedent
   further down: three letters of HOLD beside a ✕ grows the destructive
   control's footprint and its pull on the eye. The fill still runs; the
   native tooltip says "Hold to delete this run" (title.js, reading the state
   the machinery dressed). */
.slot-delete .hold-hint { display: none; }
.save-toast {
  position: fixed; top: 2rem; left: 50%; transform: translateX(-50%); z-index: 60;
  background: var(--bg-raise); border: 1px solid var(--gold); color: var(--gold);
  padding: 0.8rem 1.8rem; border-radius: var(--r-3); font-size: 1.2rem; letter-spacing: 0.1em;
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.6); animation: screenIn 200ms ease-out;
}

/* ---------- the Load / New slot list (src/ui/components/saveSlotSelector.js) --
   EVERY ROW IS THE SAME BOX. The delete used to sit BESIDE the card as a second
   flex child, with a hidden spacer holding its column open on the empty rows —
   so an occupied slot lost 4rem of text width to a button and its facts wrapped
   onto a second line while the empty rows beside it ran the full width. It now
   rides over the card's own right edge, and a row with nothing to delete
   reserves nothing. */
.title-slot-row { position: relative; display: block; }
.title-slot-row > .title-slot-pick { width: 100%; }
/* The card is what holds the row open now that the delete is out of flow, so it
   carries the button's own floor. Without it a card shorter than --iconbtn-size
   would let one row's destructive hold target reach into the next row's. */
.title-slot-row.has-delete > .title-slot-pick {
  padding-right: calc(var(--iconbtn-size) + 2.2rem);
  min-height: var(--iconbtn-size);
}
/* Position only. The box stays --iconbtn-size, which is --tap-floor, which is
   the player's Minimum tap size: a hard-coded 3.4rem here shrank a DESTRUCTIVE
   hold target to 34px and made that setting mean nothing on this one control. */
.title-slot-delete { position: absolute; top: 50%; right: 1.1rem; transform: translateY(-50%); }
/* The slot number is the row's identity and the only thing telling three rows
   apart, so it takes the glyph column the ▢/▣ checkbox-lookalike used to hold. */
.title-slot-pick .title-slot-num { flex: 0 0 2.6rem; font: 600 2.1rem/1 var(--font-display); }
/* An empty slot is not an event: it recedes so a real save is what the eye
   lands on. Dashed, muted, and still fully tappable — it is the door to a new
   climb, not a disabled row. */
.title-slot-pick.is-vacant { background: transparent; border-style: dashed; }
.title-slot-pick.is-vacant .title-slot-num,
.title-slot-pick.is-vacant .on,
.title-slot-pick.is-vacant .title-slot-state { color: var(--muted); }
.title-slot-pick.is-vacant .on { font-weight: 500; }
/* Selected is solid whatever the row holds: the dash says "nothing here yet",
   not "not selected", and a dashed gold ring under the halo read as neither. */
.title-slot-pick.is-vacant.is-selected,
.title-slot-pick.is-vacant[aria-pressed="true"] { border-style: solid; }
/* The modal divider is the rule the list hangs under. Its ◆ read as a stray dot
   floating in the gap below the head's own rule, so the diamond goes and the
   hairline stays. */
.title-menu-modal .as-decide > .as-ornament { gap: 0; margin: 0 0 1.4rem; }
.title-menu-modal .as-decide > .as-ornament span { display: none; }
/* The decision door asked its question in small grey type under a lot of air,
   with the diamond the only thing in the middle of the panel. The rule now sits
   under the head where it belongs and the question is the thing you read. */
.title-load-review .as-decide { padding-block: 1.8rem 2.2rem; }
/* In the decision door the head's own rule already closes the head, so the
   divider reads as a doubled line at full width. Inset and dimmed it is what it
   is there — a flourish over the question, not a second border. */
.title-load-review .as-decide > .as-ornament { width: 38%; margin: 0 auto 1.6rem; opacity: 0.55; }
.title-load-review .as-decide > .prompt { margin: 0; color: var(--parchment); font-size: 1.45rem; line-height: 1.5; }
.title-load-review .as-decide > .as-detailcard + .prompt { margin-top: 1.6rem; }

/* ---------- main menu ambience ---------- */
.title-screen { position: relative; overflow: hidden; }
/* THE TITLE SCREEN HAS NO BAND ABOVE THE RAIL. The utility rail's own rule in
   styles/kit.css hangs it off the bottom of the run HUD's band (`top: 100%`),
   which is what keeps it clear of the header on every surface that has one.
   The title screen has no header, and the rail is a direct child of the screen
   — so `top: 100%` would drop it a full screen height below the fold. It keeps
   its own top-right anchor, which is where it sat before the rail existed. */
.title-screen > .hud-quick-settings {
  top: calc(env(safe-area-inset-top, 0px) + (var(--hud-quick-edge-gap, 4px) / var(--ui-zoom, 1)));
}
.title-glow { animation: titleGlow 3.2s ease-in-out infinite alternate; }
@keyframes titleGlow {
  from { text-shadow: 0 0 26px rgba(201, 162, 39, 0.25); }
  to { text-shadow: 0 0 60px rgba(201, 162, 39, 0.6); }
}
.ember {
  position: absolute; bottom: -1rem; width: 0.4rem; height: 0.4rem; border-radius: var(--r-round);
  background: var(--ember); box-shadow: 0 0 8px var(--ember); opacity: 0;
  animation: emberFloat 9s linear infinite;
}
@keyframes emberFloat {
  0% { transform: translate(0, 0); opacity: 0; }
  12% { opacity: 0.8; }
  60% { opacity: 0.5; }
  100% { transform: translate(26px, -92vh); opacity: 0; }
}
@media (max-width: 30rem) {

  .title-screen { padding: 1.2rem 0.8rem 1rem; }}

/* E2 (#247): the merchant's bars ride the same fold vocabulary as the
   creation column (shop.js, THE BARS). The cap is the remove grid's own
   900px — the widest thing a bar folds — so the bars and their shelves
   agree on an edge instead of each choosing one. */
.shop-bars { max-width: 900px; }
/* The pickers CREATION folds inside a reveal wrap (E4, Law 5): these two rules
   are the creation screen's own (the fold's box is the kit's DetailCard,
   styles/kit.css), and tools/creationbrief.mjs plants against their text. */
.disc-reveal .cz-opts { flex-wrap: wrap; row-gap: 0.8rem; }
.disc-reveal .class-row { flex-wrap: wrap; row-gap: 1.2rem; }

/* The preview is one full pane: the art consumes its available area and the
   data-driven SPRITE disclosure is anchored at the bottom of that same pane. */
.cc-character-grid { min-width: 0; align-items: stretch; }
.cc-character-grid .cc-stats-side { padding: 1.4rem; }
.cc-preview-side { display: flex; flex-direction: column; min-height: 34rem; }
.cc-preview-side > .cz-portrait {
  flex: 1 1 auto;
  width: 100%;
  height: auto;
  min-height: 28rem;
  border-width: 0 0 1px;
}
.cc-preview-fold { flex: 0 0 auto; }
#cz-sprite-group > #cz-styles { width: 100%; }

/* Primary-stat faces are generated from attribute card models. Their compact
   label, summary, and value share one padded line regardless of content. */
.cc-primary-stat > .as-row.face-lite { padding: 0.75rem 1rem; flex-wrap: nowrap; }
.cc-primary-stat > .as-row.face-lite > .as-labelstack {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  align-items: baseline;
  gap: 0.8rem;
  min-width: 0;
}
.cc-primary-stat .disc-summary { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* On a desktop-wide column the summary sits on one line, so the two-line
   reserve (kit.css, "Primary stats keep this compact face") is dead height:
   five of them put the section's Continue under the fold at 1366x768
   (measured: the open PRIMARY STATS reveal stood 578px in a 594px body).
   Narrow screens keep the reserve, where the summary does wrap. */
@media (min-width: 701px) {
  .customize .cc-primary-stats .as-option.cc-primary-stat.hosts-face > .as-row { min-height: 5.6rem; }
}

@media (max-width: 30rem) {
  .cc-preview-side { min-height: 28rem; }
  .cc-preview-side > .cz-portrait { min-height: 22rem; }
}

/* EldenSpire#26 — the value the UI-size control actually applied. Muted like a
   note when the request was granted; accented when the fit had to hold it back,
   because "you asked for XL and this window gets 1.00x" is the sentence that
   stops a clamp from reading as a dead control. */
.set-applied { color: var(--muted); font-size: 1.1rem; margin-top: 0.3rem; font-variant-numeric: tabular-nums; }
.set-applied.limited { color: var(--gold); }
/* The hint stays muted even when the line around it is gold: the accent marks
   "your request was held back", and the pointer to Text size is standing
   advice, not part of that alarm. */
.set-applied .set-applied-hint { color: var(--muted); display: block; }

/* THE ONE CONTROL EXEMPT FROM THE FLOOR IT SETS.

   Marina's ruling, and the reason is deliberately the narrowest one available so
   the exemption cannot spread: THIS IS THE ONLY CONTROL IN THE GAME WHOSE
   RESIZING HAPPENS WHILE IT IS BEING PRESSED. Everything else on this screen
   still resizes under the player's eye, so nothing is lost by holding these four
   chips still.

   WHY IT IS AN EXEMPTION AND NOT AN ANCHOR. Measured: at the 44 step the pressed
   chip ended 61.44 device px to the LEFT of the finger, because every chip in the
   group re-lays-out when they all change width. That is wider than a fingertip,
   and `scrollTop` cannot answer a horizontal reflow — a row that re-lays-out is
   not a row that scrolled. A rule that holds on one axis is not held.

   KEYED ON A CHARACTERISTIC, NEVER ON THE ROW'S ID. `[data-key='tapFloor']`
   would be `if (id === …)` written in CSS — what Law 1 clause 3 forbids one layer
   down. The row DECLARES `resizesWhilePressed: true` (settings.js); this derives
   from it, so a future control with the same property is covered without an edit
   and a control WITHOUT it can never quietly acquire the exemption.

   `--tap-floor-default` is the floor frozen at `balance.ui.tapSize.def` — the same
   `def` the row's own default reads, so this is not a second copy of 44, it is the
   same 44 asked a second question. `--ui-zoom` is divided out inside it exactly as
   it is for `--tap-floor`; an exempt control that a small phone shrinks anyway
   would be an exemption in name only. */
/* THE EVENT SCREEN'S THREE BARS — Sunna's sweep of the run loop, handed over
   rather than patched because the variable this floor reads is the one this
   branch introduces.

   WHAT SHE MEASURED: 24 of 24 choice-instances under the floor, both shapes,
   all four text sizes. 23.6 / 25.8 / 27.9 / 30.1 device px on the phone — it
   never reaches 44 anywhere, and at phone Text S it is under 24, the most
   permissive step Minimum tap size is about to offer.

   WHY IT IS DANGEROUS AND NOT MERELY SMALL, which is her finding and the reason
   this is not a cosmetic fix: the three bars are ADJACENT with 9-11 px gaps, so
   a missed tap does not land on nothing — IT LANDS ON THE NEIGHBOURING CHOICE.
   Between "Pray" and "Leave" that is a permanent curse instead of walking away,
   with no confirm step, one-handed, mid-run.

   LAW 4 WAS INVERTED HERE ON BOTH CLAUSES AND THE FIX IS THREE LINES:
     clause 1  the label was an inline `13px` — it did NOT answer Text size.
               1.3rem is that same size at Text M (root 62.5%), so M is
               unchanged to the pixel and S/L/XL now move the letters.
     clause 3  the box was `.subtle`'s `padding: 0.6rem 1.4rem` — it DID answer
               Text size. px here, because a finger does not get bigger when a
               player raises Text size.
     clause 4  a floor, and `height: auto` so the box still grows on its own
               glyphs. It can neither clip nor balloon.

   SCOPE, said out loud: `.subtle`'s rem padding is untouched everywhere else.
   Converting it globally is the blanket diff Law 4 tells nobody to attempt on
   the strength of the law alone, and this is a ratchet on a screen already
   open. THE FLOOR IS THE SIZE FIX AND NOT THE CONSEQUENCE FIX — adjacency and
   the missing confirm step are Sunna's, and bigger targets do not make a curse
   reversible. */
/* `button.ev-choice`, not `.ev-choice` — AND I FOUND THAT BY MEASURING, NOT BY
   READING. `button.subtle` in base.css is (0,1,1) and a bare class is (0,1,0),
   so the first version of this rule lost BOTH the font-size and the padding to
   it silently: the label came back 12px at Text M where the old inline value
   was 13, and the box was still growing on `.subtle`'s rem padding. The floor
   applied, so the table looked fixed while two thirds of the fix was not there.
   A specificity loss is the quietest possible failure — nothing errors, the
   rule is simply not the one that won. */
button.ev-choice {
  min-height: var(--tap-floor); height: auto;
  font-size: 1.3rem;
  padding: 8px 20px;
}

/* ---------- event screen ---------- */
.event-art { display: inline-block; animation: artFloat 4s ease-in-out infinite; }
@keyframes artFloat { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
#choices button { animation: screenIn 260ms ease-out backwards; }

/* multiple player seats sit side-by-side in the combat player-zone */
.combat.coop .player-zone {
  display: flex; flex-direction: row; flex-wrap: wrap; gap: 1.4rem;
  min-width: 13rem; align-items: flex-end; justify-content: center;
}
/* Phones cannot honestly hold two player seats beside three enemies: both
   groups have real min-content widths from their resource bars. Stack the two
   rows only for co-op, keeping every fighter at the shared sprite/text sizes.
   The field owns vertical reach if an unusually short phone cannot hold both.
   The hand remains its existing horizontal scroller; inline padding gives a
   rotated first/last card a whole-card resting position at either edge. */
:root[data-layout='narrow'] .combat.coop .field {
  flex-direction: column; min-height: 0; overflow-y: auto;
}
/* BOTH ZONES YIELD BEFORE THE FIELD SCROLLS. `flex: N 0 auto` on either row
   meant neither could give a pixel, so at 390x844 the two together wanted
   449px of a 407px field and the enemy row ran 42px past the bottom — its
   nameplates were clipped at the field's edge with the flask row sitting
   under them, so "Blight Hound" and "Grave Wisp" read as half a line of
   letters. They shrink now, in the order the text law sets: the row gives
   height, the combatant is already capped to its row (COMBAT narrow), and the
   sprite inside it yields with `object-fit: contain` — so the ART loses the
   pixels and the name, meters and intent keep theirs. The field's own
   `overflow-y: auto` above stays the last resort, for a phone genuinely too
   short to hold both groups even at their smallest. */
:root[data-layout='narrow'] .combat.coop .player-zone {
  width: 100%; min-width: 0; flex: 0 1 auto; min-height: 0; flex-wrap: nowrap;
  align-items: flex-end; justify-content: space-evenly; padding-bottom: 0.6rem;
}
:root[data-layout='narrow'] .combat.coop .enemy-row {
  width: 100%; min-width: 0; flex: 1 1 auto; min-height: 0; padding-bottom: 0.6rem;
}
:root[data-layout='narrow'] .combat.coop .hand { padding-inline: 1rem; }
.combat.coop .combatant.player { position: relative; }
.combat.coop .combatant.player.me .sprite { filter: drop-shadow(0 0 6px rgba(200,160,70,.55)); }
.combat.coop .combatant.player.ended { opacity: .62; }
.combat.coop .combatant.player.down { filter: grayscale(1); opacity: .45; }
.combat.coop .combatant.player.away { opacity: .45; }
.combat.coop .combatant.player.throw-target { cursor: pointer; outline: 2px solid var(--gold); outline-offset: 4px; border-radius: var(--r-3); box-shadow: 0 0 12px rgba(200,160,70,.4); }
.combat.coop .combatant.enemy.selected-target { outline: 2px solid var(--gold); outline-offset: 3px; border-radius: var(--r-3); }
.combat.coop .coop-leave { min-height: var(--tap-floor); height: auto; }

/* co-op: fork voting on the shared map */
.map-node.my-vote > circle { stroke: var(--gold); stroke-width: 3; }
.vote-pips { pointer-events: none; }
.combatant.enemy.acting .sprite > img { animation: enemy-lunge 400ms cubic-bezier(.25,.8,.35,1); }
@keyframes enemy-lunge {
  0% { transform: translateX(0); }
  35% { transform: translateX(-22px) scale(1.05); filter: brightness(1.3); }
  100% { transform: translateX(0); }
}


#cz-equipment-receipts, .armoury-equipment-receipts { font-size: max(13px, 1em); }
#cz-equipment-receipts small, .armoury-equipment-receipts small { font-size: max(13px, 0.84em); line-height: 1.35; }

@media (max-width: 700px) {
  .armoury-equipment-receipts, .armoury-equipment-receipts * { min-width: 0; overflow-wrap: anywhere; }
  .armoury-equipment-receipts .equip-role-receipts > div { grid-template-columns: minmax(0, 1fr); }
  .armoury-equipment-receipts .equip-role-receipts small { grid-column: 1; }
  .equipmentReceiptsCard .character-info-label { min-width: 0; white-space: normal; overflow-wrap: anywhere; }}

/* ---------- ATTRIBUTES & RESOURCES — a label and its value are a PAIR --------
   Both screens that print `model/statProjection` — the Armoury strip and the
   creation screen's `#cz-stat-projection` — shipped these two blocks with NO
   RULE AT ALL, so every separator was whatever the markup happened to leave
   behind: zero. Measured at dev 3bafb25, 1440x860 and 390x844, by reading the
   rendered text runs rather than the source:

     STR 13DEX 10CON 12WIS 10INT 10
     Draw / turn and opening hand3 + 2 tier x 1 = 5The current engine uses this…

   Sunna read that off a screenshot she was taking of something else (#305) and
   handed it over rather than banking it.

   THIS IS MR-76 ONE BLOCK OVER — "printed with 0px between them and read as one
   word", the same sentence `.equip-resource-change small` above already carries.
   So nothing is invented here: the derived row copies `.equip-role-receipts >
   div`, which has the identical <b>/<span>/<small> shape and lives in this same
   strip, and the attribute row copies `.player-poise-receipt ul` — a wrapping
   row of chips, which is what a phone needs at 390 where a nowrap row would run
   off the side. One home, because the two screens print one read model. */
.statproj-attributes { display: flex; flex-wrap: wrap; gap: 0.2rem 1.1rem; }
.statproj-derived {
  display: grid; gap: 0.35rem;
  /* BOUNDED, and the bound is the point. `.equip-role-receipts` gets its width
     from `flex: 1 1 28rem`; this block has no flex parent, so an unbounded
     1fr/2fr split put `Mana` and its formula ~400 px apart across empty panel
     at 1440 — separated, and still not a pair you can read at a glance. */
  max-width: 44rem;
}
.statproj-derived > div { display: grid; grid-template-columns: minmax(9rem, 1fr) 2fr; gap: 0.1rem 0.8rem; }
.statproj-derived span, .statproj-derived small { color: var(--muted); }
.statproj-derived small { grid-column: 2; }
.arcane-exposure-meter, .arcane-exposure-immune {
  min-width: 7.2rem; max-width: 100%; box-sizing: border-box;
  overflow-wrap: anywhere; color: var(--frost); font-size: 0.92rem;
}
.arcane-exposure-immune { display: flex; gap: 0.3rem; align-items: center; }
.magic-vulnerable-receipt, .arcane-exposure-event { color: var(--muted); line-height: 1.2; }
.arcane-refusal-receipt { color: var(--gold); }

.ef-tags { display: flex; gap: 0.3rem; flex-wrap: wrap; }
.ef-run { color: var(--grace); font-size: 1.15rem; }


/* ---------- EldenSpire#29 slice 3 — the orphaned class card -----------------
   Sunna, 2026-08-01, naming a thing that had never been put in front of anyone:
   three cards wrapped 2-then-1 and the lone card was centred under the pair.
   "A lone centred card under two others is the grammar of THIS ONE IS SELECTED"
   — and it isn't; the selected card is Reaver, top-left, with a full card height
   between the gold border and where the eye lands. A layout that says something
   false about state. Constantine opened it: "approved and get it done."

   ~~ struck: the two rules this replaces, kept visible because their reasoning
   was right and only their premise expired. ~~
   ~~ .class-row { flex-wrap: wrap; justify-content: center }  — "wrapping puts
      two on a row at 433 and keeps each card at its authored width" ~~
   ~~ .class-pick { width: 17rem }  — "17rem fits two and halves the scroll.
      Chosen by measuring the scroll height, not by eye." ~~
   Both were honest measurements of how to make TWO cards share a row. The
   orphan is what the third one does, and no width fixes that.

   THE ORPHAN IS A PROPERTY OF THE COUNT, NOT OF THE WIDTH, which is why this is
   one card per row and not a cleverer wrap. Three cards wrap 2+1; four wrap 2+2
   and look fine; five wrap 2+2+1 and it is back. Law 1 promises Constantine can
   add a class by adding a table row — so a layout that is only correct at
   today's row count is a layout that breaks the next time content grows, and
   nothing would catch it. One per row is count-agnostic by construction.

   THE CARD TURNS SIDEWAYS RATHER THAN JUST GETTING WIDER, and that is the part
   that had to be measured rather than assumed: a full-width card keeping the
   big centred glyph on top is TALLER than half of a 2-up row, and would have
   pushed KEEPSAKE back below the fold — undoing what slice 2 just bought. Laid
   out as glyph-beside-text the description gets a wide measure, wraps in three
   lines instead of six, and the whole CLASS block gets SHORTER. That is the
   keepsake list's own shape, which is what Sunna pointed at: "the keepsake list
   is the proof it works."

   `.cp-body` is the only markup this needed — one wrapper so the glyph has a
   single sibling to sit beside. It is a plain block on wide and changes nothing
   there (measured: every wide rect identical). It is added in BOTH producers of
   `.class-pick`, customize.js and customRun.js, because this rule is not scoped
   to one screen — see below.

   AND IT BINDS CUSTOM CLIMB TOO, ON PURPOSE AND NOT BY ACCIDENT. `.class-pick`
   is customRun.js's card as well (`.cr-class`), with the same three classes and
   the same 2-then-1 wrap and therefore the same false "selected" grammar. A rule
   scoped to `.customize` would be the per-screen shape Marina already ruled
   against on slice 0 — four chances to remember and one to forget. Its cards
   carry no description, so they simply come out shorter; measured, not assumed.

   No width, no height, no reserved anything: the row is a column, the card is
   `auto`, the text column is `1fr`. The 17rem above was the last magic number on
   this screen and it leaves with the wrap that needed it (Law 2). */
:root[data-layout='narrow'] .class-row {
  flex-direction: column; flex-wrap: nowrap; align-items: stretch; gap: 1.2rem; width: 100%;
}
:root[data-layout='narrow'] .class-pick {
  width: auto; padding: 1.2rem 1.6rem;
  display: flex; align-items: center; gap: 1.6rem; text-align: left;
}
:root[data-layout='narrow'] .class-pick.shrine-fold {
  display: block; padding: 0; text-align: left;
}
:root[data-layout='narrow'] .class-pick .glyph { margin-bottom: 0; flex-shrink: 0; }
/* min-width: 0 or the text column refuses to shrink below its longest word and
   the card overflows its own row — the flex default that has bitten this repo
   before. It is a bound, not a length. */
:root[data-layout='narrow'] .cp-body { flex: 1; min-width: 0; }

@media (max-width: 720px) {

  .overlay-settings-actions { gap: 3px; }
  .ov-quick-action { min-width: var(--tap-floor); padding-inline: 6px; }
  .ov-qa-label { display: none; }

  .set-action-status { max-width: 11rem; text-align: right; }}
.set-notice { color: var(--gold); line-height: 1.4; margin-bottom: 8px; }

/* A FLEX CHILD CAN STILL PAINT INTO THE HUD. 320x640 at Text XL proved the
   battlefield's first intent badge crossing the HUD boundary: both 44x44
   charge flasks stayed visible, but elementFromPoint at either control's
   centre returned the enemy intent instead. The HUD is the earlier, fixed
   interaction band; keep it above the flexible battlefield layer (z-index 1
   in combat.css) so visible flask controls remain the actual hit targets.
   Scoped to the combat HUD: the map header shares `.topbar` but has no field. */
.combat .topbar.combat-hud {
  position: relative;
  z-index: 2;
}

/* ---------- the build stamp (src/ui/components/buildstamp.js) ---------------
   ONE RULE FOR THREE SURFACES. The stamp appears on the title screen, in the
   map header and in the combat HUD, and it is the same string in the same
   register everywhere — its own class here rather than three per-screen rules,
   for the same reason the markup has one home.

   THE REGISTER IS THE SEED'S, deliberately: `.map-header .mh-seed` is the
   closest sibling this game already ships — a monospace diagnostic string a
   player reads back to us — so this borrows its size and its colour instead of
   inventing a fourth treatment.

   AND IT IS ABSENT FROM EVERY NARROW HIDE RULE ON PURPOSE. The seed
   (`.mh-seed`, `.fight-label`) is hidden at `data-layout='narrow'` because it
   is recoverable from the Stats panel two taps away. The build stamp is not
   recoverable from anywhere — it is the one thing a screenshot has to carry —
   and a phone photograph is the commonest bug report there is. If a later hand
   wants it gone on a phone, that is a decision to take in the open, not a rule
   to inherit by pattern-matching on the seed. */
.build-stamp {
  color: var(--muted);
  font-family: monospace;
  font-size: 1.1rem;
  letter-spacing: 0.04em;
  white-space: nowrap;
  flex-shrink: 0;
}
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
/* On the title screen it is the last line under the flavour text, centred with
   the stack rather than floated into a corner — a corner is where a watermark
   goes, and this is meant to be read. */
.title-screen .build-stamp { margin-top: 0.4rem; text-align: center; }
/* ---------- AshenSpire folded title menu ------------------------------- */
/*
   The cold-boot gate is the folded card; this title surface is the unfurled
   hall. Both keep the existing save/content records and the shared input
   cursor. Layout is percentage/px based so it stays honest under the game's
   body zoom at wide and narrow sizes.
*/
.title-screen {
  position: relative;
  isolation: isolate;
  gap: 0;
  /* The lockup centres in what is left after this padding, so an 8rem bottom
     was pushing the wordmark UP into the backdrop's focal mass (Constantine,
     2026-09-04: "NAME IS NOT LINED UP"). Even padding centres it. */
  padding: 2.2rem 1.6rem;
  background:
    linear-gradient(180deg, rgba(12, 9, 6, 0.22), rgba(10, 8, 6, 0.96)),
    url('../assets/bg/title-city-tower.webp') center / cover;
}
.title-screen::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* The vignette's clear centre sat at 18% — the same band the wordmark lands
     in, so the art stayed brightest exactly under the name. Centred on the
     lockup instead: the art reads at the edges, the name reads over it. */
  background: radial-gradient(ellipse at 50% 45%, rgba(0, 0, 0, 0.42) 0, rgba(0, 0, 0, 0.28) 40%, rgba(0, 0, 0, 0.68) 100%);
}
.title-screen > * { position: relative; z-index: 1; }

@media (max-width: 30rem) {
  .title-screen { padding: 1.2rem 0.8rem 1rem; }}
@media (max-width: 30rem) {
  .startup-subtitle { font-size: 0.9rem; letter-spacing: 0.2em; }
  .startup-rule { width: 90%; }

  .title-screen { padding: 1.2rem 0.8rem 1rem; }}

/* The legacy viewer-facing figure and the Armoury's generated equipment figure
   keep their handedness correction. Combat is different: its painted figures
   and pose frames are already authored facing right, into the battlefield, so
   the combat player cancels this mirror immediately below.

   `.class-sprite .equipped-figure { transform: none; }` USED TO SIT HERE and is
   deleted. It existed to cancel an ANCESTOR mirror back when `.class-sprite`
   itself carried one and combat nested `.equipped-figure` inside it. Neither
   half is true now: #590 made the class figures paintings, so `equippedFigure()`
   has one call site left — the Armoury preview, which mounts it into
   `.armoury-figure`, never inside `.class-sprite` — and #618 moved the mirror
   onto `.class-sprite > .facing`. The rule matched nothing, and a reader would
   reasonably have believed the nested case still existed. */
.class-sprite > .facing,
.equipped-figure { transform: scaleX(-1); }
.combatant.player .class-sprite > .facing { transform: none; }

button.armoury-position-card { display: grid; grid-template-columns: var(--armoury-position-label-width) var(--armoury-position-sprite-width) minmax(0, 1fr); padding: 0; text-align: left; }

/* ---- the smith's card services (mount-service-modal) -------------------
   W1j / W1k: the same W1 frame as the upgrade (kit.css SMITH); only the pane
   differs: the item's mount selector, then (seating) the deck cards that
   mount takes. The instruction is the pane's idle text, not a body row. */
.mount-row-list { display: flex; flex-direction: column; gap: 6px; margin: 6px 0 10px; }
.mount-row {
  display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: 10px; min-height: var(--tap-floor);
  padding: 8px 10px; border: 1px solid var(--line, rgba(255,255,255,0.14)); border-radius: 8px;
  background: rgba(0,0,0,0.25); cursor: pointer;
}
.mount-row:focus-visible, .mount-row:hover { border-color: var(--gold); }
.mount-row.selected { border-color: var(--gold); background: rgba(201,162,39,0.12); }
.mount-row > em { grid-row: 1 / span 2; align-self: center; font-style: normal; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.mount-row > b { font-size: 13px; }
.mount-row > small { font-size: 11px; color: var(--muted); }
.mount-row.is-extra > em { color: var(--gold); }
.mount-card-list { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin: 6px 0 10px; }
.mount-card-list .card { cursor: pointer; }
/* The picked card wears the game's shared chosen ring (`.is-chosen`, kit.css); the stables no longer draw a gold one of their own. */

/* ── CENTRED + TRACKED TEXT, AND THE HALF-TRACKING BUG ────────────────────
   `letter-spacing` adds its space after the LAST glyph as well as between the
   others, so a centred tracked string sits left of true centre by half its
   tracking. THE STARTUP SCREEN LEARNED THIS FIRST and compensates with
   `text-indent` — which is exact, and here is why, because the file had two
   different answers to it and only one can be right:

     available width  = W - t   (the indent is taken off the line's room)
     line placed at   = t + (W - t - L) / 2      L includes the trailing t
     ink centre       = t + (W-t-L)/2 + (L-t)/2  =  W/2      ✔ exact

   So `text-indent: <the full tracking>` centres the INK. Some rules used HALF
   the tracking instead, and half under-corrects — the same arithmetic gives
   `W/2 - t/4`, so `.startup-subtitle` at 0.32em/0.16em sat 0.08em left of
   centre UNDER a wordmark that was corrected in full. That disagreement
   between two lines of one logo is what reads as "the logo is off centre"
   (Constantine, 2026-09-03) — neither line looks wrong alone.

   Declare the tracking once as `--track` and indent by the SAME token when
   the line has room. The kit's full-width TitleMenu name instead shifts by
   half its tracking: an indent there exceeds the nowrap line's available
   width, causing the browser to stop centering it. */


.armoury-smithing-receipt {
  display: grid; gap: 4px; padding: 8px 10px; border: 1px solid var(--gold);
  border-radius: var(--r-2); background: color-mix(in srgb, var(--panel) 92%, transparent);
  color: var(--parchment);
}

.armoury-smithing-receipt b { color: var(--gold); }

/* Shared modal polish: warm metal edges, clear selected and keyboard states. */
.modal > .modal-head {
  background: linear-gradient(110deg, rgba(var(--accent-rgb), 0.07), transparent 70%);
}
.modal > .modal-foot {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.01), rgba(0, 0, 0, 0.09));
}
.modal .modal-tabs { gap: .5rem; }
.modal .modal-tab[aria-selected='true'] {
  background: linear-gradient(0deg, rgba(var(--accent-rgb), 0.125), transparent);
  border-radius: var(--r-2) var(--r-2) 0 0;
}
.modal .modal-tab:focus-visible,
.modal .modal-close:focus-visible,
.modal .modal-iconbtn:focus-visible,
.modal .set-tab:focus-visible {
  outline: var(--focus-ring);
  outline-offset: -3px;
}
.modal .set-railed .set-panel { scrollbar-gutter: stable; }
/* Settings owns one vertical scrollport: the content pane. The old shell and
   pane both scrolled, producing the paired scrollbars visible in the report.
   The host is `[data-settings-host]` — the container renderSettings filled —
   so BOTH doors get it: the Settings modal's `.set-body` and the in-run
   overlay's `.overlay-body`. Scoping these to `.settings-modal` gave the
   overlay a second scrollbar and none of the chrome below. */
/* THE SETTINGS WINDOW'S OWN BOX, keyed to the rung it ASKED for rather than
   the one in force: the Modals width choice may have moved it, and these rules
   are this door's contract (its width setting, its height, the veil's room),
   not the rung's. The width takes the smaller of the rung in force
   (`--modal-rung-width`, published by the ladder in kit.css) and the player's
   own Window width, so the two settings compose instead of cancelling. */
.modal.settings-modal[data-authored-size='lg'] {
  width: min(var(--modal-rung-width, 76rem), calc(var(--settings-window-width, 100vw) / var(--ui-zoom, 1)), 100%);
  height: min(calc(var(--settings-window-height, 100dvh) / var(--ui-zoom, 1)), calc(100dvh / var(--ui-zoom, 1) - var(--veil-pad-top, 8px) - var(--veil-pad-bottom, 8px)));
  max-height: calc(100dvh / var(--ui-zoom, 1) - var(--veil-pad-top, 8px) - var(--veil-pad-bottom, 8px));
}
:root[data-short='true'] .modal.settings-modal[data-authored-size='lg'] { height: min(calc(var(--settings-window-height, 100dvh) / var(--ui-zoom, 1)), calc(100dvh / var(--ui-zoom, 1) - var(--veil-pad-top, 8px) - var(--veil-pad-bottom, 8px))); }
.modal > [data-settings-host] { display: flex; flex-direction: column; min-height: 0; overflow: hidden; padding: 0; }
.modal > [data-settings-host] > .set-notice { flex: 0 0 auto; margin: 0; padding: .65rem var(--modal-inset); border-bottom: 1px solid var(--line-soft); background: rgba(var(--accent-rgb), .06); }
[data-settings-host] .set-railed { flex: 1 1 auto; min-height: 0; height: auto; }
[data-settings-host] .set-panel { min-height: 0; overflow-x: hidden; overflow-y: auto; }
[data-settings-host] .set-tabs { overflow-y: auto; }
[data-settings-host] .set-tabs > * { flex-shrink: 0; }
[data-settings-host] .set-tabs > .set-advanced-head { margin-left: .8rem; padding-left: .6rem; border-left: 1px solid var(--line); }
[data-settings-host] .set-tabs .set-subtabs { display: flex; flex-direction: column; gap: .4rem; width: 100%; }
[data-settings-host] .set-tabs .set-subtab { width: 100%; min-width: 0; text-align: left; white-space: normal; font-size: 1.1rem; }
[data-settings-host] .set-subtab:focus-visible { outline: var(--focus-ring); outline-offset: -3px; }
[data-settings-host] .set-panel .set-subtabs { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1rem; }
[data-settings-host] .set-subtabs { display: flex; flex-wrap: wrap; }
[data-settings-host] .set-subtabs > button { flex: 1 0 auto; }
.set-section-select, .set-topic-select { display: none; }
/* Settings has a bounded three-row navigation band on every device. */
:root [data-settings-host] .set-railed.set-railed[data-cat-nav='selector'] > .set-tabs { grid-template-columns: repeat(3, minmax(0, 1fr)); padding: .25rem .7rem; gap: .4rem; }
:root [data-settings-host] .set-railed[data-cat-nav='selector'] > .set-tabs > .set-tab { min-height: var(--tap-floor); padding: .3rem; }
.settings-modal .modal-head { padding: .5rem .8rem; min-height: 0; gap: .5rem; }
.settings-modal .modal-head-actions { flex: 1; justify-content: flex-end; gap: .35rem; min-width: 0; }
.settings-modal .modal-head-id { flex: 0 0 auto; }
.settings-modal .modal-head h2 { font-size: 1.7rem; }
.set-header-tools { display: flex; align-items: center; justify-content: flex-end; gap: .3rem; min-width: 0; }
.set-header-tools.search-open { flex: 1; }
.set-header-tools input[type='search'] { width: 100%; min-width: 0; min-height: var(--tap-floor); background: #201c15; color: #eee5d4; border: 1px solid var(--line); border-radius: var(--r-2); padding: .4rem .7rem; }
.set-header-tools input[hidden] { display: none; }
/* One quiet toolbar: equal targets, centered icons, and a single close face. */
.settings-modal .modal-head-actions { align-items: center; align-self: center; }
:is(.settings-modal, [data-settings-host]) .set-search-toggle,
:is(.settings-modal, [data-settings-host]) .set-options > summary,
.settings-modal .modal-close {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap-floor); height: var(--tap-floor);
  min-width: var(--tap-floor); min-height: var(--tap-floor);
  flex: 0 0 auto; padding: 0; margin: 0;
  border: 1px solid transparent; border-radius: var(--r-2);
  background: transparent; color: var(--parchment); box-shadow: none;
}
:is(.settings-modal, [data-settings-host]) .set-header-tools svg { width: 1.8rem; height: 1.8rem; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
:is(.settings-modal, [data-settings-host]) .set-options > summary svg { fill: currentColor; stroke: none; }
.settings-modal .modal-close > .modal-close-face { width: 100%; height: 100%; border: 0; background: transparent; }
:is(.settings-modal, [data-settings-host]) .set-search-toggle:is(:hover, :focus-visible, [aria-expanded='true']),
:is(.settings-modal, [data-settings-host]) .set-options > summary:is(:hover, :focus-visible),
:is(.settings-modal, [data-settings-host]) .set-options[open] > summary,
.settings-modal .modal-close:is(:hover, :focus-visible) { background: var(--panel); border-color: var(--line-soft); color: var(--gold); }
:is(.settings-modal, [data-settings-host]) .set-options > summary::-webkit-details-marker { display: none; }
/* The in-run overlay has no head of its own to hang the toolbar in, so the
   same tools ride one bar at the top of the panel: Download & saves at the
   left, search and options at the right. Before this they were two loose
   blocks stacked above the rail, and the icons were unstyled black-on-black. */
[data-settings-host] > .set-inline-bar {
  flex: 0 0 auto; display: flex; align-items: center; flex-wrap: wrap; gap: .5rem;
  padding: .5rem var(--modal-inset); border-bottom: 1px solid var(--line-soft);
}
[data-settings-host] > .set-inline-bar > .set-header-tools { margin-left: auto; }
[data-settings-host] > .set-inline-bar > .set-header-tools.search-open { flex: 1 1 18rem; }
.set-options { position: relative; }
.set-options > summary { list-style: none; }
.set-options-menu { position: absolute; top: 100%; right: 0; z-index: 20; width: 20rem; display: grid; gap: .4rem; padding: .6rem; border: 1px solid var(--line); border-radius: var(--r-2); background: #211d17; }
.set-general-select { width: 100%; padding: .4rem .7rem; min-height: var(--tap-floor); margin-bottom: .5rem; background: #201c15; color: #eee5d4; border: 1px solid var(--line); border-radius: var(--r-2); }
.settings-modal .modal-foot { padding: .5rem .8rem; }
.set-export-choice { display: flex; gap: .7rem; align-items: center; }
:root [data-settings-host] .set-row-dropdown { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 45%); align-items: center; gap: .8rem; }
:root [data-settings-host] .set-row-dropdown > .r-trail { width: 100%; min-width: 0; max-width: none; }
.set-choice-select { width: 100%; min-width: 0; min-height: var(--tap-floor); padding: .4rem .6rem; background: #201c15; color: var(--text, #eee5d4); border: 1px solid var(--line); border-radius: var(--r-2); font: inherit; }
.set-export-body { padding: 1rem; }
.set-general-pickers { display: flex; gap: .5rem; }
.set-general-pickers select { flex: 1 1 0; min-width: 0; }
@media (max-width: 600px) {
  .settings-modal .modal-head:has(.set-header-tools.search-open) .modal-head-id { display: none; }
  .settings-modal .modal-head:has(.set-header-tools.search-open) .set-header-tools { width: 100%; }
}
@media (min-width: 800px) {
  [data-settings-host] .set-advanced-group[data-advanced-panel='Classes'] .set-topic-panel:not([hidden]) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 1.5rem; }
  [data-settings-host] .set-advanced-group[data-advanced-panel='Classes'] .set-topic-panel::before { grid-column: 1 / -1; }
}
.set-mobile-pickers { display: none; }
.set-railed[data-cat-nav='selector'] .set-mobile-pickers { display: flex; gap: .5rem; }
.set-railed[data-cat-nav='selector'] .set-mobile-pickers select { flex: 1 1 0; min-width: 0; }
.set-railed[data-cat-nav='selector'] .set-topic-select[hidden] { display: none; }
.set-topic-tabs { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 .8rem; }
.set-topic-tabs > button[aria-selected='true'] { color: var(--accent); border-color: var(--accent); }
.set-group-summary { display: flex; gap: .6rem; align-items: baseline; justify-content: space-between; margin: 0 0 .6rem; color: var(--muted); font-size: 1.05rem; }
.set-group-summary .set-config-count { flex: none; white-space: nowrap; }
.set-topic-panel[hidden], .set-topic-panel .set-row[hidden], .set-advanced-group[hidden], .set-subsection-heading[hidden] { display: none; }
/* Advanced → Stats: the live worked example above each trait's controls, and
   the subsection headings between its groups of rows. */
.set-example { margin: 0 0 .8rem; padding: .7rem .85rem; border: 1px solid var(--line-soft); border-left: 3px solid var(--gold); border-radius: var(--r-2); background: var(--panel); }
.set-example-head { display: flex; flex-wrap: wrap; gap: .4rem .8rem; align-items: baseline; justify-content: space-between; }
.set-example-head > span, .set-example-who { color: var(--muted); }
.set-example-who select { margin-left: .4rem; padding: .15rem .35rem; color: inherit; background: var(--panel); border: 1px solid var(--line-soft); border-radius: var(--r-2); font: inherit; }
.set-example-attrs { margin: .3rem 0 .5rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.set-example-block + .set-example-block { margin-top: .6rem; padding-top: .6rem; border-top: 1px solid var(--line); }
.set-example-block.is-off { opacity: .6; }
.set-example-title { font-weight: 600; }
.set-example-title small, .set-example-lines small { color: var(--muted); font-weight: 400; }
.set-example-lines { margin: .3rem 0 0; }
.set-example-lines > div { display: grid; grid-template-columns: minmax(7rem, max-content) 1fr; gap: .15rem .7rem; padding: .15rem 0; }
.set-example-lines dt { color: var(--muted); }
.set-example-lines dd { margin: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.set-example-total { color: var(--gold); white-space: nowrap; }
.set-example-hint { margin: .35rem 0 0; color: var(--muted); font-style: italic; }
.set-example-refused { margin: 0 0 .35rem; padding: .3rem .5rem; border-left: 3px solid var(--danger); color: var(--parchment); }
.set-example-problem { border-left-color: var(--blood); }
@media (max-width: 520px) { .set-example-lines > div { grid-template-columns: 1fr; } }
/* The Levelling preview (SPEC §15.2): the curve as a wrapping grid of steps. */
.set-level-pace-curve { display: grid; grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); gap: .25rem .5rem; margin: .35rem 0 0; padding: 0; list-style: none; font-variant-numeric: tabular-nums; }
.set-level-pace-curve li { display: flex; flex-wrap: wrap; gap: 0 .35rem; align-items: baseline; padding: .15rem .3rem; border: 1px solid var(--line); border-radius: var(--r-2); }
.set-level-pace-curve span { color: var(--muted); }
.set-level-pace-curve b { color: var(--gold); }
.set-level-pace-curve small { color: var(--muted); font-size: .8em; }
.set-level-pace-points { font-style: normal; }
.set-subsection-heading { margin: .9rem .2rem .3rem; color: var(--gold); font-size: .95rem; letter-spacing: .05em; text-transform: uppercase; }
.set-subsection-heading:first-child { margin-top: .2rem; }
.set-topic-panel[data-searching='true']::before { content: attr(data-topic-panel); display: block; color: var(--accent); padding: .7rem; }
[data-settings-host] .set-topic-panel .set-row { padding: .4rem .7rem; }
@media (min-width: 800px) { .modal.settings-modal[data-authored-size='lg'] { height: min(80rem, calc(86dvh / var(--ui-zoom, 1))); } }
.set-reset-menu { position: relative; }
.set-reset-menu summary { list-style: none; cursor: pointer; }
.set-reset-menu[open] > button { position: absolute; right: 0; top: 100%; z-index: 5; min-width: 16rem; background: var(--panel, #211d17); }
[data-settings-host] input.set-config-search { min-height: var(--tap-floor); margin: 0; padding: .6rem .8rem; background: #201c15; color: var(--text, #eee5d4); border: 1px solid var(--line); border-radius: var(--r-2); }
[data-settings-host] .set-advanced-tools { align-items: center; flex-wrap: nowrap; padding-bottom: .6rem; margin-bottom: .8rem; gap: .5rem; }
[data-settings-host] .set-config-actions { flex-wrap: nowrap; gap: .4rem; }
[data-settings-host] .set-config-search { flex: 1 1 12rem; min-width: 0; }
[data-settings-host] .set-tabs .set-subtab { min-height: 2.8rem; padding-block: .45rem; }
[data-settings-host] .set-tabs { gap: .35rem; }
:root [data-settings-host] .set-railed[data-cat-nav='selector'] > .as-catnav-toggle { display: none; }
:root [data-settings-host] .set-railed[data-cat-nav='selector'] > .set-tabs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-row: 1; padding: .6rem; gap: .4rem; border-right: 0; border-bottom: 1px solid var(--line); overflow: visible; }
:root [data-settings-host] .set-railed[data-cat-nav='selector'] > .set-tabs > .set-tab { width: 100%; min-width: 0; padding: .5rem .25rem; text-align: center; }
:root [data-settings-host] .set-railed[data-cat-nav='selector'][data-cat-open='true'] > .set-panel { visibility: visible; }
.set-railed[data-cat-nav='selector'] .set-advanced-head, .set-railed[data-cat-nav='selector'] .set-topic-tabs { display: none; }
.set-railed[data-cat-nav='selector'] .set-section-select, .set-railed[data-cat-nav='selector'] .set-topic-select { display: block; width: 100%; min-height: var(--tap-floor); margin-bottom: .6rem; padding: .5rem; background: #201c15; color: var(--text, #eee5d4); border: 1px solid var(--line); border-radius: var(--r-2); }
.set-railed[data-cat-nav='selector'] .set-panel { padding: .7rem; }
.set-railed[data-cat-nav='selector'] .set-group-summary { flex-wrap: wrap; }
.set-railed[data-cat-nav='selector'] .set-advanced-tools .as-btn { padding-inline: .6rem; }
.set-advanced-tools { display: flex; align-items: end; gap: .8rem; flex-wrap: wrap; margin: 0 0 1rem; padding-bottom: 1rem; border-bottom: 1px solid var(--line-soft); }
.set-config-search { display: grid; gap: .35rem; flex: 1 1 18rem; color: var(--muted); font-size: 1.05rem; }
.set-config-search input { width: 100%; min-height: var(--tap-floor); }
.set-config-actions { display: flex; gap: .6rem; flex-wrap: wrap; }
.set-config-count { flex: 0 0 100%; min-height: 1em; color: var(--muted); font-size: 1.05rem; }
@media (max-width: 600px) {
  .modal.settings-modal[data-authored-size='lg'] { width: 100%; border-radius: var(--r-3); }
  [data-settings-host] .set-subtabs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; border: 0; gap: .4rem; }
  [data-settings-host] .set-subtabs > button { border: 1px solid var(--line-soft); border-radius: var(--r-2); min-width: 0; }
}
/* W1a CATEGORY NAVIGATION is the kit's (kit/categoryNav.js, kit.css "W1
   CATEGORY NAVIGATION"): a rail beside the pane, or one [Section ▾] selector
   above it (`.set-cat-select`) that opens the same `.set-tabs` as a list.
   data-settings-nav / data-nav-open mirror its state for the instruments. */
@media (max-width: 600px) {
  .modal .set-panel .r-trail:has(> .choice-group) {
    flex-basis: 100%; width: 100%; max-width: 100%;
  }
  .modal .set-panel .choice-group {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
    width: 100%; gap: .4rem; border: 0; overflow: visible;
  }
  .modal .set-panel .choice-group > button {
    margin: 0; border: 1px solid var(--line-soft); border-radius: var(--r-2);
    min-width: 0; padding-inline: .6rem;
  }
}
@media (min-width: 601px) {
  .modal .set-panel .r-trail:has(> .choice-group > button:nth-child(5)) {
    flex-basis: 100%; width: 100%; max-width: 100%;
  }
  .modal .set-panel .choice-group:has(> button:nth-child(5)) {
    display: flex; flex-wrap: wrap; width: 100%;
  }
  .modal .set-panel .choice-group:has(> button:nth-child(5)) > button {
    flex: 1 0 auto; min-width: 7rem;
  }
}
/* W1a ROW CONTROLS AT THE TAP FLOOR (--tap-floor, 44 physical px). The drawn
   chip keeps the kit's height (the floor less its segment's 2px of border);
   the finger gets the floor. Its own ::after spans the floor's height, the
   chip clips with a margin (so a long label still ellipsizes and the pseudo
   is not cut to the chip), and the segment stops clipping its chips — its
   end chips round their own corners instead. Chips sit edge to edge, so a
   chip narrower than the floor (S, L, XL: 25–31 px on wide hosts) has no
   free space beside it to borrow: it takes the floor as its width. Toggles
   are the kit's (.as-toggle::after, kit.css). */
.set-panel .as-seg { overflow: visible; }
.set-panel .as-seg > button {
  position: relative; min-width: var(--tap-floor);
  overflow: clip; overflow-clip-margin: 0.5rem;
}
.set-panel .as-seg > button::after {
  content: ''; position: absolute; left: 0; right: 0; top: 50%;
  height: max(100%, var(--tap-floor)); transform: translateY(-50%);
}
.set-panel .as-seg > button:first-child { border-top-left-radius: inherit; border-bottom-left-radius: inherit; }
.set-panel .as-seg > button:last-child { border-top-right-radius: inherit; border-bottom-right-radius: inherit; }
/* A strip of five or more chips wraps (Map zoom, Accent, UI size). Wrapped
   rows used to touch, so each chip's floor overlapped the row below it and
   the pair split the seam (measured 41 px). The rows now stand 2px apart —
   the 2px each chip is short of the floor — so every chip's floor is its own. */
.set-panel .choice-group:has(> button:nth-child(5)) { row-gap: 2px; }
/* A row's own buttons (Command log's Open, the flick distance Reset) take at
   least the floor's width: "Open" measured 43.6 px wide at 844x390. */
.set-panel .r-trail button:not(.as-toggle):not(.choice) { min-width: var(--tap-floor); }
/* ---- ONE GRAMMAR FOR EVERY VALUE (2026-09-24 revamp) ----------------------
   Every number is  −  slider  field  +  on one line under its label, at every
   width; the buttons meet the tap floor and repeat while held. A row that
   differs from its default shows a dot before its name and a Reset after it. */
[data-settings-host] .set-row-number > .as-labelstack { flex: 1 1 100%; }
[data-settings-host] .set-row-number > .r-trail { flex: 1 1 100%; max-width: 100%; margin-left: 0; }
.set-stepper { display: flex; align-items: center; gap: .45rem; width: 100%; min-width: 0; }
.set-stepper > input[type='range'] { flex: 1 1 8rem; min-width: 4rem; min-height: var(--tap-floor); margin: 0; touch-action: pan-y; accent-color: var(--accent, #c9a45c); }
:root [data-settings-host] .set-stepper > input[type='number'] { flex: 0 0 5.6rem; width: 5.6rem; min-height: var(--tap-floor); padding: .3rem .25rem; text-align: center; font-variant-numeric: tabular-nums; }
.set-stepper > .set-step { flex: 0 0 auto; min-width: var(--tap-floor); min-height: var(--tap-floor); padding: 0; font-size: 1.35rem; line-height: 1; touch-action: manipulation; user-select: none; -webkit-user-select: none; }
.set-stepper > .set-step:disabled { opacity: .4; }
/* A gated or rule-locked field locks its buttons and slider with it. */
.set-stepper:has(> input[type='number']:disabled) > :is(.set-step, input[type='range']) { opacity: .4; pointer-events: none; }
.set-unit { color: var(--muted); margin-left: -.25rem; }
.set-mod-dot { display: none; width: .5rem; height: .5rem; margin-right: .45rem; border-radius: 50%; background: var(--accent, #c9a45c); vertical-align: middle; }
.set-row[data-modified='true'] .set-mod-dot { display: inline-block; }
/* The label and its Reset share one line, so showing or hiding the Reset
   never changes the row's height. The label keeps its own ellipsis. */
.set-label-line { display: flex; align-items: center; gap: .5rem; min-width: 0; }
.set-label-line > .ls-label { flex: 0 1 auto; min-width: 0; }
.set-row-reset { flex: 0 0 auto; min-height: 1.9rem; padding: .1rem .6rem; font-size: .85rem; }
/* Hidden, a Reset keeps its height (the tap floor) but no width: the row is
   the same height changed or not, and the label keeps the whole line. */
[data-settings-host] .set-label-line > .set-row-reset[hidden] { display: inline-flex !important; visibility: hidden; width: 0; min-width: 0; padding: 0; margin: 0; border: 0; overflow: hidden; }
.set-row-reset[hidden] { display: none; }
.set-search-where { margin-top: 1.1rem; }
/* The Changed filter, the Undo bar and the first-open tip. */
[data-settings-host] .set-changed-toggle { white-space: nowrap; }
[data-settings-host] .set-changed-toggle.on, [data-settings-host] .set-changed-toggle[aria-pressed='true'] { color: var(--accent, #c9a45c); border-color: var(--accent, #c9a45c); }
[data-settings-host] > :is(.set-undo, .set-tip) { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin: .4rem var(--modal-inset, .8rem); padding: .45rem .7rem; border: 1px solid var(--line-soft); border-left: 3px solid var(--accent, #c9a45c); border-radius: var(--r-2); background: var(--panel); }
[data-settings-host] > :is(.set-undo, .set-tip) > span { flex: 1 1 14rem; min-width: 0; }
[data-settings-host] > :is(.set-undo, .set-tip) > .as-btn { min-height: var(--tap-floor); }
.set-search-empty { padding: 1rem .7rem; }
/* Defaults & sync */
.set-sync { display: grid; gap: .6rem; }
.set-sync a { color: var(--accent, #c9a45c); overflow-wrap: anywhere; }
.set-sync-acts { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
.set-sync-acts .set-text { flex: 1 1 12rem; min-width: 0; min-height: var(--tap-floor); }
.set-sync-select { flex: 1 1 10rem; min-width: 0; min-height: var(--tap-floor); max-width: 100%; padding: .4rem .6rem; background: #201c15; color: var(--text, #eee5d4); border: 1px solid var(--line); border-radius: var(--r-2); font: inherit; }
.set-sync-active { color: var(--gold); overflow-wrap: anywhere; }
.set-sync-preview { margin: .2rem 0 .6rem; padding: .7rem .85rem; border: 1px solid var(--line-soft); border-left: 3px solid var(--gold); border-radius: var(--r-2); background: var(--panel); }
.set-sync-diff { margin: .4rem 0; padding-left: 1.1rem; display: grid; gap: .2rem; }
.set-sync-diff s { color: var(--muted); }
.set-sync-where { padding: .6rem .7rem; }
.set-sync-fields { display: grid; gap: .5rem; margin-top: .6rem; }
.set-sync-fields label { display: grid; gap: .2rem; color: var(--muted); }
.set-sync-fields .set-text { width: 100%; min-height: var(--tap-floor); }
@media (max-width: 600px) {
  /* Phones: a row is label over control, the control full width. */
  [data-settings-host] .set-panel .as-row.setting { flex-direction: column; align-items: stretch; }
  [data-settings-host] .set-panel .as-row.setting > .as-labelstack { flex: 0 0 auto; }
  [data-settings-host] .set-panel .as-row.setting > .r-trail { max-width: 100%; margin-left: 0; justify-content: flex-start; flex-wrap: wrap; }
  [data-settings-host] .set-panel .as-row.setting:has(> .r-trail > .as-toggle) { flex-direction: row; align-items: center; }
  [data-settings-host] .set-panel .as-row.setting:has(> .r-trail > .as-toggle) > .r-trail { margin-left: auto; }
  /* A switch row keeps label and switch side by side: the label takes the
     rest of the width and wraps, never its one-line length (was 1,395px). */
  [data-settings-host] .set-panel .as-row.setting:has(> .r-trail > .as-toggle) > .as-labelstack { flex: 1 1 0; min-width: 0; }
  [data-settings-host] .set-panel .choice-group { flex-wrap: wrap; }
  :root [data-settings-host] .set-stepper > input[type='number'] { flex-basis: 4.6rem; width: 4.6rem; }
  :root [data-settings-host] .set-row-dropdown { grid-template-columns: 1fr; }
}
/* LIVE PREVIEW STRIP (src/ui/components/settingsPreview.js) — above General ›
   Display and Accessibility. It only arranges the sample; every face in it is
   the game's own rule, restyled by the classes/vars applyDisplaySettings writes. */
[data-settings-host] .set-preview { margin: .6rem 0; border: 1px solid var(--line); border-radius: var(--r-2, 8px); background: var(--bg); min-width: 0; max-width: 100%; box-sizing: border-box; }
[data-settings-host] .set-preview > .set-preview-summary { display: flex; align-items: center; min-height: var(--tap-floor, 44px); padding: 0 .9rem; cursor: pointer; font: 600 1.1rem/1 var(--font-body); letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
[data-settings-host] .set-preview > .set-preview-body { display: flex; align-items: flex-start; gap: 1rem; padding: 0 .9rem .9rem; min-width: 0; }
[data-settings-host] .set-preview-text { display: flex; flex-direction: column; gap: .5rem; flex: 1 1 auto; min-width: 0; }
[data-settings-host] .set-preview-heading { white-space: normal; overflow-wrap: anywhere; }
[data-settings-host] .set-preview-lore { margin: 0; color: var(--parchment); 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); }
[data-settings-host] .set-preview-buttons { display: flex; flex-wrap: wrap; gap: .5rem; }
[data-settings-host] .set-preview-card { flex: 0 0 auto; width: min(var(--card-w-glance, 152px), 40%); min-width: 0; }
[data-settings-host] .set-preview-card > .card { width: 100%; max-width: 100%; margin: 0; }
@media (max-width: 600px) {
  [data-settings-host] .set-preview > .set-preview-body { padding: 0 .7rem .7rem; gap: .7rem; }
}
.set-color { width: 64px; min-height: var(--tap-floor, 44px); padding: 3px; cursor: pointer; }
:root .choice.on, :root [role='tab'][aria-selected='true'], :root .mount-row.selected,
:root .hand .card.selected { border-color: var(--selection-color, #59bd75); }
:root .combatant.context-selected .sprite { --selection-glow: drop-shadow(0 0 6px var(--selection-color, #59bd75)); }
:root .combatant.context-selected .nm { color: var(--selection-color, #59bd75); }
:root .card.selected, :root .card[data-selected='true'], :root .as-card[data-selected='true'],
:root .as-option[aria-checked='true'], :root .as-option[aria-pressed='true'], :root .as-option.is-selected,
:root .class-pick.selected, :root .class-pick[aria-checked='true'], :root .as-railitem[aria-current='true'] {
  border-color: var(--selection-color, #59bd75);
  --selection-glow: drop-shadow(0 0 6px var(--selection-color, #59bd75));
}
:root .choice.on, :root [role='tab'][aria-selected='true'], :root .as-railitem[aria-current='true'] { color: var(--selection-color, #59bd75); }
/* Existing selection treatments share a color without recoloring rarity or
   unselected gold ornamentation elsewhere in the game. */
:root :is(.selected, .is-selected, .reward-selected, .selected-target, .skill-selected,
  .tooltip-selected, [data-selected='true'], [aria-selected='true'], .choice.on,
  .modal-tab.on, .as-tab.on, .as-railitem.on, .as-railitem[aria-current='true'],
  .as-option[aria-checked='true'], .as-option[aria-pressed='true'], .class-pick[aria-checked='true']) {
  --gold: var(--selection-color, #59bd75);
  --selection-glow: drop-shadow(0 0 6px var(--selection-color, #59bd75));
}
.set-row:has(.flick-practice) { display:flex; flex-wrap:wrap; }
.set-row:has(.flick-practice) > .as-labelstack { flex-basis:100%; }
.flick-practice { display:flex; flex-direction:column; justify-content:flex-end; gap:8px; flex-basis:100%; order:2; width:100%; box-sizing:border-box; min-height:210px; padding:16px; border:1px dashed var(--gold); border-radius:8px; touch-action:none; user-select:none; }
.flick-practice[data-ready='true'] { border-style:solid; box-shadow:inset 0 0 12px #c9a84c55; }
.flick-practice output { min-height:3em; font-size:14px; }
/* Stats labels (hand rules, conversions) stay readable in the compact settings pane. */
[data-advanced-panel="Stats"] .ls-label { white-space: normal; overflow: visible; text-overflow: clip; }
[data-advanced-panel="Stats"] [aria-disabled="true"] { opacity: .5; }
.modal.hand-discard-modal { background: var(--bg-raise); }
.hand-discard-modal .modal-head h2 { white-space: normal; }
.hand-discard-item { display: flex; flex-direction: column; gap: 8px; align-items: center; width: min(100%, calc(220px / var(--ui-zoom, 1))); }
.hand-discard-item label { cursor: pointer; min-height: 44px; }
.hand-discard-item input { width: 20px; height: 20px; accent-color: #c5a32b; }

/* ---- THE DECK EDITOR (SPEC §14.1 UX; src/ui/screens/deckEditor.js) ----------
   Every target is at least 48 CSS px on a coarse pointer and 44 otherwise, ON
   GLASS: the lengths divide by the app's zoom exactly as --tap-floor does, and
   never drop below the player's own Minimum tap size (--tap-floor). Text is at
   least 11 px on glass at 360x640 (FINISH §8). Collection left, deck right;
   collection on top, deck below on a portrait phone. */
.deck-editor-veil {
  --deck-editor-target: max(var(--tap-floor), calc(44px / var(--ui-zoom, 1)));
  --deck-editor-text: max(1.3rem, calc(11px / var(--ui-zoom, 1)));
  align-items: stretch;
}
@media (pointer: coarse) {
  .deck-editor-veil { --deck-editor-target: max(var(--tap-floor), calc(48px / var(--ui-zoom, 1))); }
}
.deck-editor {
  display: grid; grid-template-rows: auto auto minmax(0, 1fr) auto; gap: 0.8rem;
  width: min(100%, 120rem); height: 100%; box-sizing: border-box; padding: 1rem;
  background: var(--bg-raise); border: 1px solid var(--line); color: var(--parchment);
  font-size: var(--deck-editor-text); overflow: hidden;
}
.deck-editor-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem 1.2rem; min-width: 0; }
.deck-editor-title { margin: 0; font-size: max(1.8rem, calc(14px / var(--ui-zoom, 1))); }
.deck-editor-counter { margin: 0; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--gold); }
.deck-editor-counter[data-state="out"] { color: var(--danger); }
.deck-editor-curve { display: flex; align-items: flex-end; gap: 0.3rem; height: calc(3.2rem + 1.2 * var(--deck-editor-text)); margin-inline-start: auto; }
.deck-editor-curve-bar { display: flex; flex-direction: column; align-items: center; justify-content: flex-end; width: max(1.8rem, calc(16px / var(--ui-zoom, 1))); height: 100%; }
.deck-editor-curve-fill { display: block; width: 100%; height: calc(var(--share, 0) * 3.2rem); min-height: 1px; background: var(--gold); border-radius: 2px 2px 0 0; }
.deck-editor-curve-label { font-size: var(--deck-editor-text); color: var(--muted); line-height: 1.1; }
.deck-editor-tools { display: grid; gap: 0.4rem; min-width: 0; }
.deck-editor-chips { display: flex; align-items: center; gap: 0.4rem; overflow-x: auto; overscroll-behavior-x: contain; min-width: 0; scrollbar-width: thin; }
.deck-editor-chips-label { flex: none; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
.deck-editor-chip {
  flex: none; min-height: var(--deck-editor-target); min-width: var(--deck-editor-target); padding: 0 1rem;
  border: 1px solid var(--line-soft); border-radius: 999px; background: transparent; color: var(--parchment);
  font: inherit; cursor: pointer;
}
.deck-editor-chip.on { border-color: var(--gold); background: rgba(var(--accent-rgb), 0.18); color: var(--gold); }
.deck-editor-panes { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0.8rem; min-height: 0; }
@media (orientation: portrait) and (max-width: 760px) {
  .deck-editor-panes { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); }
}
.deck-editor-pane { display: flex; flex-direction: column; min-height: 0; min-width: 0; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); }
.deck-editor-pane.active { border-color: var(--gold); }
.deck-editor-pane-title { margin: 0; padding: 0.4rem 0.8rem; font-size: var(--deck-editor-text); text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
.deck-editor-list { display: grid; align-content: start; gap: 0.3rem; padding: 0 0.4rem 0.4rem; overflow-y: auto; overscroll-behavior: contain; min-height: 0; flex: 1; }
.deck-editor-item { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.3rem; align-items: stretch; min-width: 0; }
.deck-editor-main {
  flex: 1 1 12em; min-width: 0; min-height: var(--deck-editor-target);
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; grid-template-rows: auto auto; column-gap: 0.6rem; align-items: center;
  padding: 0.3rem 0.8rem; text-align: start; font: inherit; color: inherit; cursor: pointer;
  background: var(--bg-raise); border: 1px solid var(--line); border-radius: 4px;
}
.deck-editor-actions { display: flex; gap: 0.3rem; flex: none; }
.deck-editor-cost { grid-row: 1 / span 2; display: grid; place-items: center; width: 2.4em; height: 2.4em; border-radius: 50%; border: 1px solid var(--gold); color: var(--gold); font-weight: 700; }
.deck-editor-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.deck-editor-count { grid-column: 3; grid-row: 1 / span 2; align-self: center; font-weight: 700; color: var(--gold); font-variant-numeric: tabular-nums; }
.deck-editor-meta { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.deck-editor-step {
  flex: none; min-width: var(--deck-editor-target); min-height: var(--deck-editor-target); padding: 0 0.6rem;
  font: inherit; font-weight: 700; color: var(--parchment); background: var(--bg-raise);
  border: 1px solid var(--line-soft); border-radius: 4px; cursor: pointer;
}
.deck-editor-main[aria-disabled="true"], .deck-editor-step[aria-disabled="true"] { opacity: 0.5; cursor: not-allowed; }
.deck-editor-tile.spent .deck-editor-name { color: var(--muted); }
.deck-editor-row.held .deck-editor-main { border-color: var(--gold); box-shadow: 0 0 0 2px rgba(var(--accent-rgb), 0.5); }
.deck-editor-empty { margin: 0.6rem; color: var(--muted); }
/* The pointer drag (a finger as well as a mouse): the lifted item dims, the
   place it would land is outlined, and nothing selects text meanwhile. */
.deck-editor-dragging { user-select: none; -webkit-user-select: none; }
.deck-editor-item.drag-source { opacity: 0.45; }
.deck-editor-pane.drop-target, .deck-editor-row.drop-target { outline: 2px dashed var(--gold); outline-offset: -2px; }
.deck-editor-footer { display: grid; gap: 0.4rem; min-width: 0; }
.deck-editor-refusal { margin: 0; color: var(--danger); font-weight: 700; }
.deck-editor-refusal[hidden] { display: none; }
.deck-editor-keys { margin: 0; color: var(--muted); }
@media (pointer: coarse) { .deck-editor-keys { display: none; } }
.deck-editor-foot .as-btn { min-height: var(--deck-editor-target); font-size: max(1em, var(--deck-editor-text)); }
.armoury-edit-deck { align-self: flex-start; min-height: var(--tap-floor); }

/* SPEC §15.4 — the Armoury's Sigils panel (src/ui/screens/equipment.js
   sigilsPanel). Every button is at least 48 CSS px on a coarse pointer and 44
   otherwise, ON GLASS (divided by the app's zoom, never below the player's own
   Minimum tap size), as the deck editor's targets are. */
.armoury-sigils {
  --armoury-sigils-target: max(var(--tap-floor), calc(44px / var(--ui-zoom, 1)));
  display: grid; gap: 0.6rem; margin-top: 1rem; padding: 0.8rem;
  border: 1px solid var(--line); background: var(--bg-raise); color: var(--parchment);
}
@media (pointer: coarse) {
  .armoury-sigils { --armoury-sigils-target: max(var(--tap-floor), calc(48px / var(--ui-zoom, 1))); }
}
.armoury-sigils .armoury-sigils-count { color: var(--muted); }
.armoury-sigils .armoury-sigils-refusal { margin: 0; color: var(--parchment); font-weight: 600; }
.armoury-sigil {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.2rem 0.8rem; align-items: center;
  padding: 0.6rem 0; border-top: 1px solid var(--line);
}
.armoury-sigil > .as-prose { grid-column: 1 / -1; margin: 0; }
.armoury-sigil[data-attuned="true"] .armoury-sigil-name { color: var(--gold); }
.armoury-sigil-name { font-weight: 600; }
.armoury-sigils button { min-height: var(--armoury-sigils-target); min-width: var(--armoury-sigils-target); grid-column: 1 / -1; justify-self: start; }
