/*
 * The battle pages: the index of staged fights, and the ring that replays one.
 *
 * Almost everything that MOVES here is a one-shot CSS transition driven by the
 * step walker in battle-fight.ts, because scripts/check-fx.ts holds every
 * animation to the `.st-lit` gate and nothing in the replay loops: a lunge, a
 * hit, a damage pop and a KO each happen once, on cue, and rest. The cards
 * themselves get their tier fx through the normal gated machinery
 * (`armCardFx`), untouched.
 *
 * THE ONE EXCEPTION IS THE DIE (S5h). A tumble is two turns and four bounces,
 * which is a keyframe list and not a transition, so it takes the gate the
 * proper way: `armCardFx().observe()` lights `.bt-dice`, the animation lives
 * inside `@media (prefers-reduced-motion: no-preference)` under a `.st-lit`
 * selector, and an unlit or still-motion die just carries its number. Two
 * dice at a time, transform and opacity only — docs/DEVICES.md's floor is an
 * old Android and a fight is exactly when a phone is being stared at.
 *
 * Transforms live on `.bt-*` wrappers only — the card's one `transform` has
 * owners already (S2d's law) and the ring is not one of them.
 */

/* ---- Shared shell ---- */

/* Width is the shell's (SB1): these pages mount in `.st-stage--copy`. */
.bt-head,
main {
  padding-inline: 16px;
}

.bt-head {
  padding-top: 20px;
}

.bt-head h1 {
  margin: 0;
  font-size: var(--st-text-xl);
  line-height: var(--st-leading-tight);
}

.bt-backrow {
  margin: 0 0 6px;
}

.bt-back {
  display: inline-flex;
  align-items: center;
  min-height: var(--st-tap-min);
  color: var(--st-ink-muted);
  text-decoration: none;
  font-weight: var(--st-weight-medium);
}

.bt-back:hover {
  color: var(--st-ink);
}

.bt-count {
  margin: 4px 0 0;
  color: var(--st-ink-muted);
  font-size: var(--st-text-sm);
}

.bt-note,
.bt-demo {
  margin: 12px 0 0;
  padding: 10px 12px;
  border: var(--st-line) solid var(--st-rule);
  border-radius: var(--st-radius-md);
  background: var(--st-surface-sunk);
  color: var(--st-ink-muted);
  font-size: var(--st-text-sm);
}

.bt-section {
  margin: 24px 0 8px;
  font-size: var(--st-text-lg);
}

.bt-empty {
  margin-top: 32px;
  text-align: center;
  color: var(--st-ink-muted);
}

/* ---- The tier chip, coloured from the tokens ---- */

.bt-tier {
  display: inline-block;
  padding: 1px 8px;
  border-radius: var(--st-radius-pill);
  border: var(--st-line) solid var(--st-rule-strong);
  background: var(--st-tier, var(--st-surface-sunk));
  color: var(--st-tier-ink, var(--st-ink));
  font-size: var(--st-text-xs);
  font-weight: var(--st-weight-medium);
  white-space: nowrap;
}

.bt-tier[data-tier='common'] {
  --st-tier: var(--st-tier-common);
  --st-tier-ink: var(--st-tier-common-ink);
}
.bt-tier[data-tier='uncommon'] {
  --st-tier: var(--st-tier-uncommon);
  --st-tier-ink: var(--st-tier-uncommon-ink);
}
.bt-tier[data-tier='rare'] {
  --st-tier: var(--st-tier-rare);
  --st-tier-ink: var(--st-tier-rare-ink);
}
.bt-tier[data-tier='epic'] {
  --st-tier: var(--st-tier-epic);
  --st-tier-ink: var(--st-tier-epic-ink);
}
.bt-tier[data-tier='legendary'] {
  --st-tier: var(--st-tier-legendary);
  --st-tier-ink: var(--st-tier-legendary-ink);
}
.bt-tier[data-tier='mythic'] {
  --st-tier: var(--st-tier-mythic);
  --st-tier-ink: var(--st-tier-mythic-ink);
}
.bt-tier[data-tier='secret'] {
  --st-tier: var(--st-tier-secret);
  --st-tier-ink: var(--st-tier-secret-ink);
}

.bt-chip {
  display: inline-block;
  padding: 1px 8px;
  border-radius: var(--st-radius-pill);
  border: var(--st-line) solid var(--st-rule-strong);
  background: var(--st-surface);
  color: var(--st-ink-muted);
  font-size: var(--st-text-xs);
  white-space: nowrap;
}

.bt-chip--gate {
  border-color: var(--st-warn);
  background: color-mix(in srgb, var(--st-warn) 18%, var(--st-surface));
  color: var(--st-ink);
}

/* ---- The index: how it works, leagues, fight rows ---- */

.bt-steps {
  margin: 0;
  padding-left: 22px;
}

.bt-steps li {
  margin-bottom: 4px;
}

.bt-house-note {
  margin: 10px 0 0;
  color: var(--st-ink-muted);
  font-size: var(--st-text-sm);
}

.bt-league {
  margin-top: 24px;
  padding: 14px 14px 6px;
  border: var(--st-line) solid var(--st-rule-strong);
  border-radius: var(--st-radius-lg);
  background: var(--st-surface);
}

.bt-league__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0;
  font-size: var(--st-text-lg);
}

.bt-league__stakes {
  margin: 6px 0 0;
  color: var(--st-ink-muted);
  font-size: var(--st-text-sm);
}

/* The battle page's hardcore box wears the same red. */
.bt-league[data-league='hardcore'] {
  border-color: color-mix(in srgb, var(--st-bad) 55%, var(--st-rule-strong));
}

.bt-league[data-league='hardcore'] .bt-league__head {
  color: var(--st-bad);
}

.bt-fights {
  list-style: none;
  margin: 12px 0 0;
  padding: 0;
}

.bt-fights li {
  border-top: var(--st-line) solid var(--st-rule);
}

.bt-fightrow {
  display: block;
  padding: 10px 2px;
  min-height: var(--st-tap-min);
  color: inherit;
  text-decoration: none;
  border-radius: var(--st-radius-sm);
}

.bt-fightrow:hover {
  background: var(--st-surface-sunk);
}

.bt-fightrow__title {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  font-weight: var(--st-weight-medium);
}

.bt-fightrow__watch {
  color: var(--st-focus);
  font-size: var(--st-text-sm);
  white-space: nowrap;
}

.bt-fightrow__match {
  margin-top: 2px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  color: var(--st-ink-muted);
  font-size: var(--st-text-sm);
}

.bt-fightrow__note {
  margin-top: 2px;
  color: var(--st-ink-muted);
  font-size: var(--st-text-xs);
}

/* ---- The ring ---- */

.bt-ring {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-top: 16px;
}

.bt-vs {
  position: absolute;
  top: 96px;
  left: 50%;
  translate: -50% 0;
  z-index: 3;
  padding: 3px 8px;
  border: var(--st-line-thick) solid var(--st-accent);
  border-radius: var(--st-radius-pill);
  background: var(--st-surface);
  font-weight: var(--st-weight-bold);
  font-size: var(--st-text-xs);
  letter-spacing: 0.08em;
}

.bt-side {
  min-width: 0;
  transition: transform 160ms ease-out;
}

/* The attacker leans in; the defender flinches. One shot each, on cue. */
.bt-side--lunge-right {
  transform: translateX(7%) rotate(1.2deg);
}

.bt-side--lunge-left {
  transform: translateX(-7%) rotate(-1.2deg);
}

.bt-side--hit {
  transform: translateX(0) scale(0.985);
  transition-duration: 90ms;
}

/*
 * The KO tips the CARDBOX, not the whole side: the side wrapper is ~330px
 * tall with the plate and bar in it, and rotating that swings a corner
 * ±17px — past the viewport edge on the 360 floor. The cardbox is short
 * enough that 5° stays inside the column (measured against DEVICES.md's
 * floor in battle.browser.ts, at the FINAL state where the tip is standing).
 */
.bt-side--ko .bt-cardbox {
  transform: rotate(5deg) translateY(4px) scale(0.98);
  filter: grayscale(0.7);
  opacity: 0.75;
  transition:
    transform 700ms cubic-bezier(0.34, 1.2, 0.64, 1),
    filter 700ms ease,
    opacity 700ms ease;
}

.bt-plate {
  margin: 0 0 6px;
  min-height: 1.2em;
  font-size: var(--st-text-sm);
  font-weight: var(--st-weight-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bt-plate__who {
  color: var(--st-ink-muted);
}

.bt-cardbox {
  position: relative;
}

.bt-pops {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}

.bt-pop {
  position: absolute;
  top: 34%;
  left: 50%;
  translate: -50% 0;
  padding: 2px 8px;
  border: var(--st-line-thick) solid var(--st-surface);
  border-radius: var(--st-radius-pill);
  background: var(--st-bad);
  color: var(--st-bad-ink);
  font-family: var(--st-font-numeric);
  font-weight: var(--st-weight-bold);
  font-size: var(--st-text-md);
  opacity: 0;
  transform: translateY(10px) scale(0.85);
  transition:
    transform 620ms cubic-bezier(0.22, 0.9, 0.36, 1),
    opacity 180ms ease-out;
}

.bt-pop--soft {
  background: var(--st-warn);
  color: var(--st-warn-ink);
}

.bt-pop--fly {
  opacity: 1;
  transform: translateY(-30px) scale(1);
}

.bt-pop--fade {
  opacity: 0;
  transition: opacity 220ms ease-in;
}

/* ---- Health ---- */

.bt-hp {
  margin-top: 8px;
}

.bt-hpbar {
  height: 10px;
  border: var(--st-line) solid var(--st-rule-strong);
  border-radius: var(--st-radius-pill);
  background: var(--st-surface-sunk);
  overflow: hidden;
}

.bt-hpfill {
  height: 100%;
  background: var(--st-good);
  transform-origin: left center;
  transform: scaleX(1);
  transition:
    transform 320ms ease-out,
    background-color 320ms ease-out;
}

.bt-hpfill--warn {
  background: var(--st-warn);
}

.bt-hpfill--low {
  background: var(--st-bad);
}

.bt-hpnum {
  margin: 3px 0 0;
  font-size: var(--st-text-xs);
  color: var(--st-ink-muted);
}

/* THE TRAY (docs/ITEMS.md): always under your card in a live fight, one slot
 * per item. Greyed when you hold none or it is not your moment; lit when the
 * tap for your own swing is yours and you have been hit. Hendo asked for
 * greyed-out rather than absent: the tray is also what you HOLD, and a thing
 * you hold does not vanish because you cannot use it this second. */
.bt-items {
  margin: 6px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.bt-item {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 4px 10px 4px 6px;
  border: var(--st-line) solid var(--st-rule-strong);
  border-radius: var(--st-radius-pill);
  background: var(--st-surface);
  color: var(--st-ink);
  font: inherit;
  font-size: var(--st-text-xs);
  cursor: pointer;
  opacity: 0.45;
  transition:
    opacity 160ms ease-out,
    box-shadow 160ms ease-out;
}

.bt-item img {
  width: 28px;
  height: 28px;
  object-fit: contain;
}

.bt-item__count {
  font-family: var(--st-font-numeric);
  font-weight: var(--st-weight-medium);
}

.bt-item[disabled] {
  cursor: default;
}

.bt-item--held {
  opacity: 0.7;
}

.bt-item--lit {
  opacity: 1;
  border-color: var(--st-good);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--st-good) 35%, transparent);
}

/* The tray while a drink is on offer (2026-09-12): the whole strip says now. */
.bt-items--window {
  padding: 6px;
  margin-inline: -6px;
  border-radius: var(--st-radius-md);
  background: color-mix(in srgb, var(--st-good) 12%, transparent);
}

.bt-item:focus-visible {
  outline: 2px solid var(--st-focus);
  outline-offset: 2px;
}

/* A drink: the number goes UP, in the good colour. A shield and a strength
 * potion say what they did in the same voice, in their own colours. */
.bt-pop--heal {
  background: var(--st-good);
  color: var(--st-good-ink);
}

.bt-pop--shield {
  background: var(--st-tier-rare);
  color: var(--st-tier-rare-ink);
}

.bt-pop--buff,
.bt-pop--debuff {
  background: var(--st-warn);
  color: var(--st-warn-ink);
}

.bt-chips {
  margin: 6px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  font-size: var(--st-text-xs);
  color: var(--st-ink-muted);
}

/* ---- The beat strip ---- */

.bt-beat {
  margin-top: 14px;
  min-height: 76px;
  padding: 10px 12px;
  border: var(--st-line) solid var(--st-rule);
  border-radius: var(--st-radius-md);
  background: var(--st-surface);
  text-align: center;
}

.bt-dice {
  margin: 0 0 6px;
  display: flex;
  justify-content: center;
  gap: 14px;
}

/*
 * THE DIE (S5h). docs/BATTLE-DICE.md: "The die tumbles and settles on the
 * number the server already chose. It must NEVER show a number it then
 * corrects — a die that lands on 4 and flicks to 6 is worse than no animation,
 * because it tells a child the game is lying to them."
 *
 * So the tumble shows NO number. `rollDie()` writes the face once, before the
 * die starts moving, and the pip is hidden while it moves — there is never a
 * second number to correct, and an animation cut short by Skip shows the
 * right one because it was already there.
 *
 * The tumble and the hiding are in ONE gated block below, so an unlit die (no
 * IntersectionObserver, off screen) or a reduced-motion one simply carries its
 * number with no movement. That is the complete floor, not a degraded one.
 */
.bt-die {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 38px;
  height: 38px;
  padding-inline: 6px;
  border: var(--st-line-thick) solid var(--st-rule-strong);
  border-radius: var(--st-radius-md);
  background: var(--st-surface-sunk);
  font-family: var(--st-font-numeric);
  font-weight: var(--st-weight-bold);
  font-size: var(--st-text-lg);
  transform: scale(0.9);
  transition: transform 140ms ease-out;
}

.bt-die__pip {
  transition: opacity 120ms ease-out;
}

.bt-die--set {
  transform: scale(1);
}

/* The thunk as it settles: one transition, on the transform it already owns. */
.bt-die--land {
  transform: scale(1.14);
  transition: transform var(--bt-land, 220ms) var(--st-ease-out-back, ease-out);
}

.bt-die--win {
  background: var(--st-coin);
}

/* A die whose roller is not the one on this side of the ring keeps its space
 * so the visible die stays under the card that threw it. */
.bt-die--idle {
  visibility: hidden;
}

/*
 * The tumble. Transform and opacity only — the two properties the compositor
 * can take without re-rasterising — because docs/DEVICES.md's floor is an old
 * Android and a fight is exactly when a phone is being stared at. Two dice at
 * a time, at most.
 *
 * Two whole turns, so it lands square on the face it started with, and the
 * duration comes in from `shared/battle-replay.ts` as --bt-tumble: the number
 * lives in the tested module, not here.
 */
@media (prefers-reduced-motion: no-preference) {
  .bt-dice.st-lit .bt-die--rolling {
    animation: bt-tumble var(--bt-tumble, 620ms) cubic-bezier(0.3, 0.05, 0.35, 1) 1 both;
  }

  .bt-dice.st-lit .bt-die--rolling .bt-die__pip {
    opacity: 0;
  }
}

@keyframes bt-tumble {
  0% {
    transform: translateY(0) rotate(0deg) scale(0.94);
  }
  18% {
    transform: translateY(-15px) rotate(150deg) scale(1.08);
  }
  38% {
    transform: translateY(-3px) rotate(330deg) scale(0.94);
  }
  58% {
    transform: translateY(-11px) rotate(500deg) scale(1.06);
  }
  80% {
    transform: translateY(-1px) rotate(650deg) scale(0.97);
  }
  100% {
    transform: translateY(0) rotate(720deg) scale(1);
  }
}

.bt-beatline {
  margin: 0;
  min-height: 2.6em;
  font-size: var(--st-text-sm);
}

/* ---- Whose roll it is, and how long it has (S5h) ---- */

.bt-tap {
  margin-top: 10px;
  padding: 10px 12px;
  border: var(--st-line-thick) solid var(--st-rule-strong);
  border-radius: var(--st-radius-md);
  background: var(--st-surface);
  text-align: center;
}

/* The panel is YOURS to press: the accent frames it. */
.bt-tap[data-who='you'] {
  border-color: var(--st-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--st-accent) 25%, transparent);
}

/* Your roll and their roll are two different states of the same panel, and a
 * child should be able to tell which at arm's length. */
.bt-tap[data-who='them'] {
  border-color: var(--st-rule-strong);
  border-width: var(--st-line);
  background: var(--st-surface-sunk);
}

/* TWO LINES, ALWAYS. Every state of this panel is the same height, so the
 * button does not move between steps and the page does not jump under a thumb
 * that is already reaching for it — the same reservation, for the same reason,
 * as the shelf's `.bq-slot__chips`. */
.bt-tap__line {
  margin: 0;
  min-height: 2.5em;
  font-weight: var(--st-weight-medium);
}

.bt-tapbar {
  margin: 8px 0 0;
  height: 8px;
  border: var(--st-line) solid var(--st-rule);
  border-radius: var(--st-radius-pill);
  background: var(--st-surface-sunk);
  overflow: hidden;
}

/* One one-shot drain per step, `revealsInMs` long — the duration is set inline
 * from the reveal, because the window is config and not a stylesheet number.
 * The same shape as the queue's `matchWaitSeconds` bar, and the same reason:
 * a countdown that costs the phone nothing per frame. */
.bt-tapfill {
  height: 100%;
  background: var(--st-accent);
  transform-origin: left;
  transform: scaleX(1);
}

/*
 * THE DURATION COMES IN AS A CUSTOM PROPERTY, and that is not a style choice.
 * Setting `style.transitionDuration` on the element instead leaves an inline
 * duration behind when the class comes off — and with no `transition-property`
 * beside it that is `all`, so `visibility` (which IS transitionable, and flips
 * discretely at the END of its duration) stayed `visible` for the whole twenty
 * seconds and the "no countdown running" rule below silently did nothing.
 * Found by measuring the computed style while the selector matched.
 */
.bt-tapfill--draining {
  transform: scaleX(0);
  transition-property: transform;
  transition-timing-function: linear;
  transition-duration: var(--bt-drain, 0ms);
}

.bt-tap[data-who='them'] .bt-tapfill {
  background: var(--st-rule-strong);
}

/* No countdown is running, so the track is empty rather than full — a full bar
 * beside a die already in the air reads as twenty seconds nobody has spent. */
.bt-tap[data-who='rolling'] .bt-tapfill,
.bt-tap[data-who='auto'] .bt-tapfill {
  visibility: hidden;
}

/* Reserved like the line above it: while a die is in the air there is no
 * countdown to print, and an empty <p> has no line box at all — which took
 * 27px out of the panel between steps and moved the button under the thumb.
 * Caught by the browser check that measures the panel at every step. */
.bt-tap__clock {
  margin: 6px 0 0;
  /* Exactly one line of its own type, so the reservation cannot drift from
   * what a line actually costs. */
  min-height: calc(var(--st-text-sm) * var(--st-leading-normal));
  line-height: var(--st-leading-normal);
  font-family: var(--st-font-numeric);
  font-size: var(--st-text-sm);
  color: var(--st-ink-muted);
}

.bt-tap__roll {
  display: block;
  width: 100%;
  margin-top: 8px;
  /* Room under it when the page scrolls the button into view. */
  scroll-margin-bottom: 12px;
}

/* Nobody's to press: gone, but still holding its place. `visibility` and not
 * `hidden`, because the reservation IS the point. It leaves the tab order too. */
.bt-tap__roll--idle {
  visibility: hidden;
}

.bt-tap__rule,
.bt-tap__standin {
  margin: 8px 0 0;
  color: var(--st-ink-muted);
  font-size: var(--st-text-xs);
  line-height: var(--st-leading-normal);
}

/* ---- Controls ---- */

.bt-controls {
  margin-top: 12px;
  display: flex;
  gap: 10px;
  justify-content: center;
  flex-wrap: wrap;
}

/* Above the cards since 2026-09-13, away from the Roll button: a small, quiet
   bar on the right, so a thumb reaching for the next roll cannot land on it. */
.bt-controls--top {
  margin: 0 0 12px;
  justify-content: flex-end;
}
.bt-controls--top .bt-skip__line,
.bt-controls--top .bt-skip__clock {
  text-align: end;
}
.bt-controls--top #skip,
.bt-controls--top #watch {
  font-size: var(--st-text-sm);
}

.bt-button {
  min-height: var(--st-tap-min);
  padding: 8px 18px;
  border: var(--st-line-thick) solid var(--st-accent);
  border-radius: var(--st-radius-pill);
  background: var(--st-accent);
  color: var(--st-accent-ink);
  font: inherit;
  font-weight: var(--st-weight-medium);
  cursor: pointer;
}

.bt-button--quiet {
  border-color: var(--st-rule-strong);
  background: var(--st-surface);
  color: var(--st-ink);
}

.bt-button:disabled {
  opacity: 0.5;
  cursor: default;
}

/* ---- The result ---- */

.bt-result {
  margin-top: 18px;
  padding: 14px;
  border: var(--st-line) solid var(--st-rule-strong);
  border-radius: var(--st-radius-lg);
  background: var(--st-surface);
}

/*
 * Won or lost, as a picture (S9d) — §8's `state_victory` and `state_defeat`,
 * the only two of the five that are not an empty state.
 *
 * Only the drawing and the two lines under it centre. The consequence block
 * below them is money and rules — a payout, the hardcore card, the buyback
 * window — and centring THAT would make a paragraph somebody has to read
 * carefully look like a shout. So the section stays left-aligned and this
 * centres the top of it.
 */
.bt-result > .st-state-art {
  margin-block-end: var(--st-space-2);
}

.bt-result > .st-state-art ~ .bt-outcome,
.bt-result > .st-state-art ~ .bt-scoreline,
.bt-result > .st-state-art ~ .bt-firststrike {
  text-align: center;
}

.bt-outcome {
  margin: 0;
  font-size: var(--st-text-xl);
}

.bt-outcome--lost {
  color: var(--st-bad);
}

.bt-outcome--won {
  color: var(--st-good);
}

.bt-scoreline,
.bt-firststrike {
  margin: 6px 0 0;
  color: var(--st-ink-muted);
  font-size: var(--st-text-sm);
}

.bt-consequence {
  margin-top: 10px;
}

.bt-consequence p {
  margin: 6px 0 0;
  font-size: var(--st-text-sm);
}

.bt-held {
  padding: 10px 12px;
  border: var(--st-line-thick) solid var(--st-bad);
  border-radius: var(--st-radius-md);
  background: color-mix(in srgb, var(--st-bad) 8%, var(--st-surface));
}

/* A card bought back is not bad news, and must stop being drawn as it (S5i). */
.bt-held--back {
  border-color: var(--st-good);
  background: color-mix(in srgb, var(--st-good) 8%, var(--st-surface));
}

/*
 * WHERE TO GO NEXT (S5i) — the doors off the result.
 *
 * Under a rule, because they are what to do about the result rather than more
 * of it, and full width rather than a row of centred pills: at the 360 floor
 * two `.bt-button` labels do not fit one line inside this panel's padding, so
 * a wrapping flex row would leave one pill and one orphan — and the orphan
 * would be the collection door on the screen where a child has just lost a
 * card. `auto-fit` stacks them at the floor and packs two or three across
 * on a desk with no second rule to keep in step (docs/DESKTOP.md).
 */
.bt-next {
  margin-top: var(--st-space-4);
  padding-top: var(--st-space-3);
  border-top: var(--st-line) solid var(--st-rule);
}

.bt-next__lead {
  margin: 0 0 var(--st-space-2);
  font-weight: var(--st-weight-medium);
}

.bt-next__doors {
  display: grid;
  gap: var(--st-space-2);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}

.bt-next__door {
  justify-content: center;
  text-align: center;
}

.bt-logwrap {
  margin-top: 12px;
}

.bt-logwrap summary {
  min-height: var(--st-tap-min);
  display: flex;
  align-items: center;
  cursor: pointer;
  font-weight: var(--st-weight-medium);
}

.bt-log {
  margin: 6px 0 0;
  padding-left: 22px;
  font-size: var(--st-text-sm);
}

.bt-log li {
  margin-bottom: 3px;
}

.bt-log .st-num {
  font-family: var(--st-font-numeric);
}

.bt-provenance {
  margin: 12px 0 0;
  color: var(--st-ink-muted);
  font-size: var(--st-text-xs);
}

/* ---- Wider screens: the ring grows, everything else just breathes ---- */

@media (min-width: 680px) {
  .bt-ring {
    max-width: 560px;
    margin-inline: auto;
    gap: 24px;
  }

  .bt-vs {
    top: 120px;
  }
}

/*
 * THE DESKTOP RING (Hendo, 2026-09-11: "really build out the desktop
 * version of it, it looks really incomplete"). On a 1920 screen the fight
 * was a 720px column with two 260px cards in it — a phone page in the
 * middle of a desk. From 1160 the fight's stage widens to the play width and
 * the ring to 880px, so each card is ~400px: under the hero's 420 cap, and
 * big enough that the drawing is the thing on the screen. The beat log and
 * the controls under it keep the copy width — a sentence does not want to
 * be 1160px long.
 */
@media (min-width: 1160px) {
  .bt-stage--fight {
    --st-stage-max: var(--st-stage-play);
  }

  .bt-stage--fight .bt-ring {
    max-width: 880px;
    gap: 40px;
  }

  .bt-stage--fight .bt-vs {
    top: 190px;
  }

  .bt-stage--fight .bt-head,
  .bt-stage--fight .bt-beat,
  .bt-stage--fight .bt-controls,
  .bt-stage--fight .bt-result,
  .bt-stage--fight .bt-logwrap {
    max-width: var(--st-stage-copy);
    margin-inline: auto;
  }
}

/* One-shots stand still for anyone who asked the OS for less motion. The
 * walker never runs there (replayPlan returns the result alone), so this is
 * belt and braces, not the design. */
@media (prefers-reduced-motion: reduce) {
  .bt-side,
  .bt-side--ko .bt-cardbox,
  .bt-pop,
  .bt-hpfill,
  .bt-die,
  .bt-die__pip,
  .bt-tapfill--draining {
    transition: none;
  }
}

/* An author `display` beats the UA's `[hidden] { display: none }` (the
 * workshop.css scar, found again here on S5e's own elements): every element
 * these pages toggle with `hidden` and also give a display must fall back to
 * none, or "hidden" dice, controls and the empty ring's VS keep painting. */
.bt-ring[hidden],
.bt-dice[hidden],
.bt-controls[hidden],
.bt-tap[hidden],
.bt-tap__standin[hidden],
.bq-bar[hidden] {
  display: none;
}

/* ---- The view pills (S5f): the collection pages' switcher, answered ---- */

.bt-views {
  margin-top: var(--st-space-2);
  display: flex;
  gap: var(--st-space-2);
  flex-wrap: wrap;
}

.bt-views > a,
.bt-views > span {
  display: inline-flex;
  align-items: center;
  min-height: var(--st-tap-min);
  padding: 0 var(--st-space-3);
  border: var(--st-line) solid var(--st-rule);
  border-radius: var(--st-radius-pill);
  color: var(--st-ink);
  text-decoration: none;
  font-weight: 600;
}

.bt-views > span[aria-current='page'] {
  border-color: var(--st-accent);
  background: var(--st-accent);
  color: var(--st-accent-ink);
}

/* ---- Find a fight (S5f) ---- */

.bt-ctarow {
  margin: 16px 0 0;
}

a.bt-button {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
}

/* Fill the viewport: the waiting screen is shorter than a phone, and `html`
 * carries no background, so a short body would reveal the browser canvas.
 * That was visibly wrong under the old `color-scheme: light dark`; the site is
 * light only now, so it is merely the wrong white — still worth painting, and
 * still the only screen short enough to show it. */
.bq-body {
  min-height: 100dvh;
  background: var(--st-paper);
}

.bq-main {
  /* Room for the fixed action bar, which must never cover the last shelf row. */
  padding-bottom: 104px;
}

.bq-leagues {
  display: grid;
  gap: 8px;
}

.bq-league {
  display: block;
  width: 100%;
  text-align: left;
  min-height: var(--st-tap-min);
  padding: 10px 12px;
  border: var(--st-line) solid var(--st-rule);
  border-radius: var(--st-radius-md);
  background: var(--st-surface);
  color: var(--st-ink);
  font: inherit;
  cursor: pointer;
}

.bq-league[aria-pressed='true'] {
  border-color: var(--st-accent);
  box-shadow: inset 0 0 0 1px var(--st-accent);
}

/*
 * HARDCORE IS RED (Hendo, 2026-09-12: "make hardcore a red that fits with
 * the site ... when you select it it's a red border and red text inside").
 * `--st-bad` is the site's coral, chosen against navy for the re-skin; the
 * name is red at rest so the league reads as the dangerous one before it is
 * pressed, and pressing it turns the frame, the words and a soft glow red.
 */
.bq-league[data-league='hardcore'] .bq-league__name {
  color: var(--st-bad);
}

.bq-league[data-league='hardcore'][aria-pressed='true'] {
  border-color: var(--st-bad);
  box-shadow:
    inset 0 0 0 1px var(--st-bad),
    0 0 0 3px color-mix(in srgb, var(--st-bad) 22%, transparent);
  background: color-mix(in srgb, var(--st-bad) 10%, var(--st-surface));
  color: color-mix(in srgb, var(--st-bad) 75%, var(--st-ink));
}

.bq-league[data-league='hardcore'][aria-pressed='true'] .bq-league__name {
  color: var(--st-bad);
}

.bq-league__name {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-weight: 600;
}

.bq-league__stakes {
  display: block;
  margin-top: 4px;
  color: var(--st-ink-muted);
  font-size: var(--st-text-sm);
}

/* -------------------------------------------------------------------------- */
/* WHO IS LOOKING — the lobby (2026-09-07)                                     */
/* -------------------------------------------------------------------------- */

/*
 * Measured at DEVICES.md's 360px floor, where this panel sits ABOVE the shelf
 * and must not push the cards off the screen: one row is a name, a colour chip
 * and a button, and it stacks rather than shrinking the button under 44px.
 */
.bq-lobby {
  margin-bottom: 18px;
}

.bq-lobby__list {
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 8px;
}

.bq-look {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  padding: 10px 12px;
  border: var(--st-line) solid var(--st-rule);
  border-radius: var(--st-radius-md);
  background: var(--st-surface);
}

.bq-look__body {
  display: grid;
  gap: 2px;
  /* Takes the row, so the button wraps under it at 360 instead of squeezing. */
  flex: 1 1 12rem;
  min-width: 0;
}

.bq-look__head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.bq-look__who {
  font-weight: 700;
  /* A display name is the one string on this page somebody else chose. */
  overflow-wrap: anywhere;
}

.bq-look__where {
  font-size: 0.85rem;
  color: var(--st-ink-muted);
}

.bq-look__why {
  flex: 1 1 100%;
  font-size: 0.85rem;
  color: var(--st-ink-muted);
}

.bq-look__join {
  min-height: var(--st-tap-min);
  flex: 0 0 auto;
}

.bq-shelf {
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
}

.bq-slot {
  display: block;
  padding: 8px;
  border: var(--st-line) solid var(--st-rule);
  border-radius: var(--st-radius-md);
  background: var(--st-surface);
  color: inherit;
  font: inherit;
  cursor: pointer;
  text-align: center;
}

.bq-slot .st-card {
  margin-inline: auto;
}

.bq-slot[aria-pressed='true'] {
  border-color: var(--st-accent);
  box-shadow: inset 0 0 0 1px var(--st-accent);
}

.bq-slot:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

.bq-slot__chips {
  display: flex;
  gap: 6px;
  justify-content: center;
  flex-wrap: wrap;
  margin-top: 6px;
  /*
   * ALWAYS THE SAME HEIGHT, CHIPS OR NOT. The row was appended only when there
   * was something to say, so a shelf where one card is levelled and its
   * neighbour is not put the two cards at different heights inside equal
   * cells, and the levelled one read as the bigger card (Hendo, 2026-09-05:
   * "level seems to be making the grid slot larger for cards").
   *
   * The grid was never ragged -- measured, every slot 263px and every card
   * 155x217 -- but where the card SITS in its slot was, and that is the same
   * thing to look at. Reserving the row costs one line of white on the cards
   * that say nothing, and lines every card in the shelf up.
   */
  min-height: 26px;
}

.bq-chip--locked {
  border-color: var(--st-bad);
  color: var(--st-bad);
}

.bq-bar {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  /*
   * ABOVE THE SHELF IT DOCKS OVER. `position: fixed` with no z-index leaves
   * the bar's own background in the same paint layer as the cards scrolling
   * under it, and `.st-card` raises a stacking context of its own (the bob's
   * `will-change: transform`, card/frames.css). The card took the background
   * while the bar's text and button still painted on top, so the dock read as
   * transparent with card art crossing it -- Hendo, 2026-09-05, on the queue:
   * "See how the find a fight looks buggy?"
   *
   * One BELOW the phone nav bar (`--st-z-sticky`, shell/nav.css): the two dock
   * edge to edge and must never trade places. It stays inside the column, so
   * the nav sheet's scrim still dims it -- the scrim ranks higher again.
   *
   * HIT-TESTING CANNOT SEE THIS. `elementFromPoint` returned the bar at every
   * point across it, cards behind and all, both before the fix and after; only
   * a rendered screenshot showed the card winning. Measure this one by eye, or
   * by pixels -- never by what the page says is on top.
   */
  z-index: calc(var(--st-z-sticky) - 1);
  display: flex;
  align-items: center;
  justify-content: space-between;
  /*
   * IT WRAPS, since the bar grew a second button (2026-09-07, "Fight the
   * House"). At 390px the line wants two lines of its own and two buttons want
   * about 250px beside it; without this they are squeezed into an unreadable
   * column of one word each. Wrapped, the line takes the width it needs and
   * the two controls sit together on the row under it.
   */
  flex-wrap: wrap;
  gap: 12px;
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
  background: var(--st-paper);
  border-top: var(--st-line) solid var(--st-rule);
}

.bq-bar__line {
  margin: 0;
  min-width: 0;
  font-weight: 600;
  font-size: var(--st-text-sm);
  /*
   * TWO LINES, NOT AN ELLIPSIS. Measured at 390px: the label wants 225px and
   * gets 189, so a single nowrap line cut "Shark Tod · the Coins league" off
   * mid-word right beside the button and read as the bar overlapping itself
   * (Hendo, 2026-09-05: "the navigation at the bottom looks off").
   *
   * An ellipsis would have been honest and still lost the league, which is
   * half of what the line is FOR: which card, in which league. It wraps
   * instead, and the bar grows by one line on the phones that need it.
   */
  white-space: normal;
  line-height: 1.25;
  overflow-wrap: anywhere;
}

.bq-bar .bt-button {
  white-space: nowrap;
  flex-shrink: 0;
}

/*
 * The two ways to start a fight, kept together so they wrap as a pair rather
 * than one of them stranding on a row of its own. `margin-left: auto` keeps
 * them at the far end while the line is beside them, and does nothing once the
 * bar has wrapped and they have the row to themselves.
 */
.bq-bar__acts {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  flex-wrap: wrap;
}

.bq-waitline,
.bq-foundline {
  margin: 8px 0 0;
}

/*
 * The wait, with §8's hourglass over it (S9d).
 *
 * The drawing goes above the heading and everything under it centres with it —
 * this section is one moment on the screen, not a column of facts, and a
 * centred picture over a left-aligned paragraph reads as two blocks that
 * happened to land together. The bar keeps its full width; it is the clock.
 */
.bq-waiting {
  text-align: center;
}

.bq-waiting .st-state-art {
  margin-block-end: 0;
}

.bq-waitbar {
  margin-top: 12px;
  height: 10px;
  border: var(--st-line) solid var(--st-rule);
  border-radius: var(--st-radius-pill);
  background: var(--st-surface-sunk);
  overflow: hidden;
}

/* One one-shot drain, `matchWaitSeconds` long — the duration is set inline
 * from the snapshot, because the number is config, not stylesheet. */
.bq-waitfill {
  height: 100%;
  background: var(--st-accent);
  transform-origin: left;
  transform: scaleX(1);
}

.bq-waitfill--draining {
  transform: scaleX(0);
  transition-property: transform;
  transition-timing-function: linear;
}

.bq-opponent {
  margin-top: 12px;
  display: grid;
  justify-items: center;
  gap: 6px;
}

.bq-oppcard {
  width: min(60vw, 240px);
}

.bt-payout {
  margin: 0 0 8px;
  font-size: var(--st-text-lg);
}

@media (min-width: 680px) {
  .bq-leagues {
    grid-template-columns: 1fr 1fr;
  }

  /*
   * THE SHELF PACKS BY WIDTH, IT DOES NOT COUNT TO FOUR.
   *
   * `repeat(4, …)` inside a content region that stops at 720px made the card
   * on this shelf get SMALLER as the screen got bigger — measured across the
   * widths docs/DEVICES.md names: 140px at 360, 167px at 414, and then 145px
   * at every desktop from 768 to 1920. The cliff was one pixel wide: 300px at
   * a 679px viewport, 135px at 680, because crossing the breakpoint doubled
   * the column count while the shelf grew by one pixel.
   *
   * `auto-fit` with a stated minimum adds a column only when there is room for
   * a real card in it, which is the shape docs/DESKTOP.md says to take from
   * the other project before anything else. 200px is chosen against the
   * region, not guessed: at the 720 maximum it packs three, giving a 203px
   * card — under §1.1's 240px desktop cap, and above the 167px a big phone
   * already got, so the card never shrinks as the screen grows again.
   *
   * The phone rule above is deliberately untouched. It is an explicit 2-up
   * because 2-up at the 360 floor is a measurement (158px slots, 140px cards)
   * and not something worth re-deriving from a minimum every time.
   */
  .bq-shelf {
    grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr));
  }

  /*
   * The CARD stops at §1.1's 240 even where the TRACK runs on (SB2). `1fr`
   * above is what packs columns by width, but between two column counts a
   * track can pass 240 — measured at 265px at an 852 viewport, and at ~294px
   * inside the desk's narrower main below — and a queue card bigger than any
   * collection card is the trade backwards. The slot already centres its
   * card, so the cap costs air at the slot's edges, not alignment.
   */
  .bq-slot .st-card {
    max-width: 240px;
  }
}

/*
 * THE DESK (SB2, docs/DESKTOP.md: "the shelf and the card you have picked").
 *
 * From 960 the queue is two regions: the shelf of your battleable cards on
 * the left, and the join panel on the right — the same #bar element that is
 * a fixed bottom bar on a phone, standing still beside the shelf where a
 * mouse expects controls to stay. One element, two postures, so
 * battle-queue.ts never learns the difference: it fills the same line and
 * enables the same button either way.
 *
 * The join panel is hand-dressed rather than wearing `.st-panel--wide`
 * because that variant flips at 720, and between 720 and 960 this element is
 * still the FIXED bar — a floating bordered slab over the shelf is exactly
 * what it must not become. Every number below is a token.
 */
@media (min-width: 960px) {
  .bq-desk {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    column-gap: var(--st-space-6);
    align-items: start;
  }

  /*
   * The desk SETS A PLACE when the join panel arrives: while nothing is
   * picked the shelf has the whole stage (a blank 360px column beside a
   * two-thirds-empty page was the first cut, and it read as a mistake), and
   * the moment battle-queue.ts un-hides the bar the side column exists for it
   * to stand in. `:has()` because the bar's visibility is the page's own
   * state machine and the layout must follow it, not re-model it; on an
   * engine without `:has()` the desk simply stays one column and the bar
   * lays out beside the main in an implicit track — narrower, never broken.
   */
  .bq-desk:has(.bq-bar:not([hidden])) {
    grid-template-columns: minmax(0, 1fr) 360px;
  }

  .bq-desk > .bt-head {
    grid-column: 1 / -1;
  }

  .bq-desk .bq-main {
    grid-column: 1;
    grid-row: 2;
    /* The fixed bar is gone from the bottom edge; its 104px reservation with it. */
    padding-bottom: var(--st-space-7);
  }

  .bq-desk .bq-bar {
    position: static;
    grid-column: 2;
    grid-row: 2;
    flex-direction: column;
    align-items: stretch;
    gap: var(--st-space-3);
    padding: var(--st-space-4);
    border: var(--st-line) solid var(--st-rule);
    border-radius: var(--st-radius-lg);
    background: var(--st-surface);
    box-shadow: var(--st-shadow-card);
  }

  .bq-desk .bq-bar__line {
    font-size: var(--st-text-md);
  }

  .bq-desk .bq-bar .bt-button {
    width: 100%;
  }

  /* The waiting and found stories read down a centred column; the shelf's
     width is for cards, not for a sentence about one fight. */
  .bq-desk .bq-waiting,
  .bq-desk .bq-found {
    max-width: var(--st-stage-copy);
    margin-inline: auto;
  }
}

@media (prefers-reduced-motion: reduce) {
  .bq-waitfill--draining {
    transition: none;
  }
}

/* ---- S5j: the offer to skip to the result -------------------------------- */
/*
 * Hendo, 2026-09-05: "let's not allow skip to result if it's player vs player
 * unless both players agree." Two lines and a second button, inside the
 * controls row that already held Skip — the question belongs where the button
 * that asks it is, not up in the tap panel, which is about the die.
 *
 * `flex-basis: 100%` puts each line on a row of its own inside the wrapping
 * flex row, so the sentence sits ABOVE the buttons at every width rather than
 * squeezing them onto a second line at 360px.
 *
 * Both start hidden, and stay hidden whenever the copy has nothing to say, so
 * the controls keep exactly the height they had against the house — where this
 * whole feature stands down.
 */

.bt-skip__line,
.bt-skip__clock {
  flex-basis: 100%;
  margin: 0;
  text-align: center;
  line-height: var(--st-leading-normal);
}

.bt-skip__line {
  font-size: var(--st-text-sm);
  color: var(--st-ink);
}

/* The countdown reads as a number, like the tap panel's, and quieter than the
 * sentence above it: it is the rule's clock, not the question. */
.bt-skip__clock {
  font-family: var(--st-font-numeric);
  font-size: var(--st-text-xs);
  color: var(--st-ink-muted);
}

.bt-skip__line[hidden],
.bt-skip__clock[hidden] {
  display: none;
}
