/*
 * selfgrow — the card table.
 *
 * The whole game is one card lying on a dark table: a nameplate, a type line,
 * the rules, the open screen, and a rail of actions on its right. The palette in
 * :root is the only place on the page a colour is written, so every screen added
 * later belongs to the same card instead of looking bolted on.
 *
 * The type is Bricolage Grotesque, served from docs/fonts/ rather than a font
 * CDN: nothing on this page waits on a request that leaves its own origin.
 */

@font-face {
  font-family: "Bricolage Grotesque";
  font-style: normal;
  font-weight: 200 800;
  font-display: swap;
  src: url("./fonts/bricolage-grotesque.woff2") format("woff2");
}

:root {
  color-scheme: dark;

  /* The table the card lies on, and the card itself. */
  --table: #0f1322;
  --table-lit: #1b2240;
  --card: #222a45;
  --card-sheen: rgba(255, 255, 255, 0.05);
  --edge: #3a4570;
  --edge-soft: rgba(168, 177, 212, 0.18);

  /* Foil is the accent, spent on the numbers and the one main action. */
  --foil: #f2c14e;
  --foil-lit: #f7cf68;
  --on-foil: #141a2e;

  --ink: #eef1fb;
  --ink-muted: #a8b1d4;

  /* Each opponent has an accent of its own, spent on its portrait and on the
     option picked in the Play screen's picker — so who is being chosen is read
     from a figure and a word as well as from a colour. */
  --opponent-thornmere: #7fd6a6;
  --opponent-emberwake: #ffa06b;
  --opponent-mistfen: #8fc7e6;
  --opponent-barrowmere: #c4a0e8;
  --opponent-rootwarden: #a9c98a;
  --opponent-mossheart: #c3e07a;
  --opponent-twinbough: #f0a4c0;
  /* Quillmere's accent is parchment, the colour of the scroll its deck keeps
     handing it, told apart from every other opponent's colour. */
  --opponent-quillmere: #e6c58a;
  /* Galehollow's accent is the clear blue of open sky, told apart from
     Mistfen's paler fen-blue by its deeper, colder cast. */
  --opponent-galehollow: #7fb4ff;

  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --radius-card: 20px;
  --radius-control: 12px;

  /* The one width every card face in a match is drawn at, so the same card is the
     same size in hand and on either table: the picture fills that width at the
     drawing's own 96/64 ratio. */
  --match-face: minmax(8.5rem, 9.5rem);

  -webkit-tap-highlight-color: rgba(242, 193, 78, 0.18);
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: clamp(1rem, 3vw, 2.5rem);
  background-color: var(--table);
  background-image: radial-gradient(120% 90% at 50% 0%, var(--table-lit), var(--table) 65%);
  color: var(--ink);
  font-family: "Bricolage Grotesque", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-optical-sizing: auto;
  font-size: clamp(1rem, 0.95rem + 0.2vw, 1.0625rem);
  line-height: 1.5;
}

::selection {
  background-color: rgba(242, 193, 78, 0.35);
  color: var(--ink);
}

/* --------------------------------------------------------------------------
   The card
   -------------------------------------------------------------------------- */

.card {
  position: relative;
  width: min(1280px, 100%);
  padding: clamp(1.25rem, 2.8vw, 2.9rem);
  border-radius: var(--radius-card);
  background-color: var(--card);
  background-image: linear-gradient(160deg, var(--card-sheen), transparent 45%);
  box-shadow: 0 28px 60px -28px rgba(2, 4, 12, 0.9);
}

/* The printed keyline inside a card's frame — a hairline of foil, not depth. */
.card::after {
  content: "";
  position: absolute;
  inset: clamp(0.5rem, 1vw, 0.9rem);
  border: 1px solid rgba(242, 193, 78, 0.28);
  border-radius: calc(var(--radius-card) - clamp(0.5rem, 1vw, 0.9rem));
  pointer-events: none;
}

.nameplate {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.25rem 1.5rem;
}

.wordmark {
  margin: 0;
  font-size: clamp(2.4rem, 1.2rem + 4.8vw, 4.5rem);
  font-weight: 800;
  letter-spacing: -0.025em;
  line-height: 1.05;
}

.type-line {
  margin: 0;
  color: var(--ink-muted);
  font-size: clamp(0.85rem, 0.8rem + 0.2vw, 0.95rem);
  letter-spacing: 0.06em;
}

.rules {
  max-width: 62ch;
  margin: 0;
  padding-block: clamp(0.9rem, 1.6vw, 1.4rem) clamp(1.1rem, 2.2vw, 1.8rem);
  border-bottom: 1px solid var(--edge-soft);
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.15rem);
  text-wrap: pretty;
}

/* --------------------------------------------------------------------------
   The open screen
   -------------------------------------------------------------------------- */

.screen {
  display: grid;
  gap: clamp(1rem, 2vw, 1.6rem);
  padding-top: clamp(0.9rem, 2vw, 1.6rem);
  animation: screen-in 200ms var(--ease-out) both;
}

.screen[hidden] {
  display: none;
}

.screen-heading {
  margin: 0;
  font-size: clamp(1.05rem, 0.95rem + 0.5vw, 1.3rem);
  font-weight: 700;
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.screen-note {
  margin: 0;
  color: var(--ink-muted);
}

/* On a wide window the numbers sit left and the actions on a rail to the right:
   where you stand on one side, what you can do on the other, one glance. */
@media (min-width: 720px) {
  .screen {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: end;
  }

  .screen-heading {
    grid-column: 1 / -1;
  }

  /* Home keeps its shape with the deck added: where you stand on the left,
     what you can do on the right, and the deck's cards in the left column under
     the numbers it restates. */
  #screen-home .deck-summary,
  #screen-home .home-deck {
    grid-column: 1;
  }

  #screen-home .actions {
    grid-column: 2;
    grid-row: 2 / span 2;
  }
}

@keyframes screen-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* The same entrance without the move. A filled animation keeps its transform on
   the element, and a transformed ancestor is the containing block for a fixed
   descendant — which on a phone would drag the match's docked action bar off the
   foot of the window. The match screen fades instead of sliding, so the bar sits
   where the viewport is. */
@keyframes screen-fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

.standings {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(1.25rem, 3vw, 2.75rem);
  margin: 0;
}

.standing {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.standing-label {
  color: var(--ink-muted);
  font-size: 0.9rem;
  letter-spacing: 0.02em;
}

.standing-value {
  margin: 0;
  color: var(--foil);
  font-size: clamp(1.7rem, 1.3rem + 1.4vw, 2.4rem);
  font-weight: 700;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------------------
   The collection
   -------------------------------------------------------------------------- */

/* The counts and the cards belong to each other and to the whole card, not to
   the left column the home screen's numbers sit in. */
#screen-collection .standings,
#screen-collection .deck,
#screen-collection .set,
#screen-collection .deck-message,
#screen-collection .storage-note,
#screen-collection .save-restore {
  grid-column: 1 / -1;
}

/* "Cards collected" reads as one number out of another, so the "of" between them
   stays quiet and the two figures carry the weight. */
.standing-of {
  color: var(--ink-muted);
  font-size: 0.6em;
  font-weight: 600;
}

/* The whole set, under the deck: its heading and then the cards themselves, in
   the same column as everything else on the screen. */
.set {
  display: grid;
  gap: 0.6rem;
}

.set-heading {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* The view buttons above the set: a small row that switches the list between
   the whole set, what is still to collect, and the cards that share an ability
   — when played, mending, when they fall or growing. They wrap onto more than
   one row at a phone's width rather than squeezing. */
.collection-view {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

/* The pressed view reads as chosen without colour alone: it is filled, and its
   aria-pressed state says the same to a screen reader. */
.collection-view .btn[aria-pressed="true"] {
  background-color: var(--foil);
  border-color: var(--foil);
  color: var(--on-foil);
}

/* How much of an ability view the player already holds, e.g. "2 of 5
   collected": a quiet line under the view buttons, so it reads as a caption on
   the list rather than a heading over it. Hidden in the views that do not
   print it. */
.collection-view-count {
  margin: 0;
  color: var(--ink-muted);
  font-size: 0.85rem;
  font-weight: 600;
}

/* A view with nothing to list says so rather than reading as an empty set —
   every card collected in the "missing" view, no ability cards in the
   "abilities" one. */
.collection-empty {
  grid-column: 1 / -1;
  margin: 0;
  padding: 0.85rem 1rem;
  border: 1px dashed var(--edge-soft);
  border-radius: var(--radius-control);
  color: var(--ink-muted);
}

.collection-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.7rem;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 13.5rem), 1fr));
}

/* Each card in the collection is a small card face: its name, then the three
   numbers that decide a match, with the count of copies when there is more than
   one. A card not collected yet keeps the same face, dimmed and dashed, so the
   set reads as one list with the cards in hand standing forward in it. */
.owned-card,
.unowned-card {
  display: grid;
  gap: 0.6rem;
  padding: 0.85rem 1rem 0.95rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-control);
  background-color: rgba(15, 19, 34, 0.4);
}

/* A card not collected yet is the same face marked as a gap in the set: a
   dashed edge, a quieter ground, and its picture, name and numbers dimmed so it
   is readable without competing with the cards the player actually holds. */
.unowned-card {
  border-style: dashed;
  border-color: var(--edge-soft);
  background-color: rgba(15, 19, 34, 0.2);
}
.unowned-card .card-art,
.unowned-card .card-face-head,
.unowned-card .card-face-stats {
  opacity: 0.72;
}

.unowned-note {
  align-self: end;
  margin: 0;
  color: var(--ink-muted);
  font-size: 0.8rem;
  letter-spacing: 0.02em;
}

/* The card the most recent win added, marked where the set is listed: the word
   carries it for a screen reader, and the foil keyline and wash carry it for a
   player scanning the set for what they just earned. */
.owned-card.newly-collected {
  border-color: rgba(242, 193, 78, 0.55);
  background-color: rgba(242, 193, 78, 0.1);
}

.newly-collected-note {
  /* Held beside the card's name rather than floated into the middle of the head,
     so the mark reads as part of the card and the copy count keeps the edge. */
  margin-right: auto;
  padding: 0.05rem 0.5rem;
  border: 1px solid rgba(242, 193, 78, 0.5);
  border-radius: 999px;
  color: var(--foil);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* Which opponent a signature card stands for, named on its face in the set list:
   the character to beat for a card a player is still missing. Held beside the
   name and framed as a foil pill, the mark the same card carries in the picker's
   deck fold, so one card is marked one way wherever it is listed. */
.card-face-head .signature-note {
  /* Held beside the card's name rather than floated into the middle of the head,
     so the mark reads as part of the card and the copy count keeps the edge. */
  margin-right: auto;
  /* A long opponent name wraps inside the pill rather than off the card. */
  max-width: 100%;
  padding: 0.05rem 0.5rem;
  border: 1px solid rgba(242, 193, 78, 0.5);
  border-radius: 999px;
  color: var(--foil);
  font-size: 0.72rem;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: 0.02em;
}

/* The picture on a card face, drawn in the card's own palette by cardart.js.
   The frame is part of the drawing, so this only sizes it: full width, at the
   drawing's own 96x64 ratio, above the name it illustrates. */
.card-art {
  display: block;
  width: 100%;
  aspect-ratio: 96 / 64;
}

/* The three shapes every card is drawn from. The frame is the picture's only
   stroke; the body is the creature; the accent is the one detail it carries —
   an eye, a knot — printed in the colour the card prints on foil, so it reads
   as cut into the gold. */
.art-frame {
  fill: var(--table-lit);
  stroke: var(--edge);
  stroke-width: 1.5;
}

.art-body {
  fill: var(--foil);
}

.art-accent {
  fill: var(--on-foil);
}

.card-face-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.15rem 0.75rem;
  min-width: 0;
}

.card-face-name {
  font-weight: 700;
  overflow-wrap: anywhere;
}

.card-face-copies {
  color: var(--ink-muted);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}

.card-face-stats {
  display: flex;
  gap: 1.1rem;
  margin: 0;
}

.card-face-stat {
  display: grid;
}

.card-face-stat dt {
  color: var(--ink-muted);
  font-size: 0.78rem;
  letter-spacing: 0.04em;
}

.card-face-stat dd {
  margin: 0;
  color: var(--foil);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The ability line on a card's face: the one sentence a card carries besides
   its numbers, printed in the same words by app.js wherever the card is shown —
   collection, deck, hand and match table. A hairline above it makes it read as
   the card's text box rather than as a fourth stat, and it wraps inside the face
   instead of widening it, so a narrow phone column keeps every card the same
   size. */
.card-ability {
  margin: 0;
  padding-top: 0.4rem;
  border-top: 1px solid var(--edge-soft);
  color: var(--ink-muted);
  font-size: 0.8rem;
  line-height: 1.35;
  text-wrap: pretty;
  overflow-wrap: anywhere;
}

/* A card that carries both a when-played and a fall ability prints them as two
   lines of the one text box, the when-played line first. The box's hairline
   belongs to the whole box, so it is drawn once on the wrapper and each line
   inside carries none of its own. */
.card-abilities {
  display: grid;
  gap: 0.3rem;
  margin: 0;
  padding-top: 0.4rem;
  border-top: 1px solid var(--edge-soft);
}

.card-abilities .card-ability {
  padding-top: 0;
  border-top: 0;
}

/* The deck controls at the foot of each card face: how many of its copies are
   in the deck, then the buttons that add one more or take one out. Scoped under
   .btn as .btn.deck-add / .btn.deck-remove so these sizes outrank .btn's own
   padding and font instead of being silently overridden. */
.deck-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.4rem;
}

.owned-in-deck {
  grid-column: 1 / -1;
  color: var(--ink-muted);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
}

.btn.deck-add,
.btn.deck-remove {
  padding: 0.45rem 0.5rem;
  font-size: 0.8rem;
  line-height: 1.15;
  text-align: center;
}

/* An Add with every owned copy already placed, or a Remove with nothing in the
   deck, still presses and reports the refusal — it only reads as the action that
   will not succeed, so the button's own layer is the quiet one. */
.btn.deck-add.is-at-limit,
.btn.deck-remove.is-at-limit {
  border-color: var(--edge-soft);
  color: var(--ink-muted);
  background-color: rgba(168, 177, 212, 0.03);
}

/* A card with at least one copy in the deck carries a foil keyline, so the
   chosen ones are read at a glance even before the count is. */
.btn.deck-remove:not(.is-at-limit) {
  border-color: rgba(242, 193, 78, 0.6);
  background-color: rgba(242, 193, 78, 0.12);
}

/* --------------------------------------------------------------------------
   The deck
   -------------------------------------------------------------------------- */

.deck {
  display: grid;
  gap: 0.6rem;
  padding: 0.9rem 1rem 1rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-control);
  background-color: rgba(15, 19, 34, 0.4);
}

.deck-heading {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.deck-rule {
  margin: 0;
  color: var(--ink-muted);
  font-size: 0.9rem;
}

.deck-rule span {
  color: var(--ink);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The deck's cards as a hand: small card faces that fill the row on a wide
   window and fall to two per row on a phone, so the deck reads by sight and
   stays short to scroll instead of becoming a list of names. */
.deck-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 0.5rem;
}

/* One deck card, a small face: the picture, then its name and three numbers.
   Sized so the 96x64 drawing stays legible at two columns on a phone. */
.deck-card {
  display: grid;
  gap: 0.3rem;
  padding: 0.4rem 0.45rem 0.55rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-control);
  background-color: rgba(34, 42, 69, 0.7);
}

.deck-card-name {
  font-weight: 600;
  font-size: 0.9rem;
  line-height: 1.25;
  overflow-wrap: anywhere;
}

.deck-card-stats {
  color: var(--foil);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}

/* The control that takes one copy out of the deck, on the face it removes:
   full width under the picture and numbers so it is one easy target a thumb
   reaches, and sized down so a face at two columns on a phone still fits. */
.btn.deck-card-remove {
  width: 100%;
  min-width: 0;
  padding: 0.35rem 0.45rem;
  font-size: 0.75rem;
  line-height: 1.2;
  text-align: center;
}

/* The list takes focus when a card's last copy leaves it, so it has to show
   where focus went rather than handing a keyboard user an invisible position. */
.deck-list:focus {
  outline: 2px solid var(--foil);
  outline-offset: 3px;
  border-radius: var(--radius-control);
}

.deck-empty {
  color: var(--ink-muted);
  font-size: 0.9rem;
}

/* The control that empties the whole deck, so rebuilding one does not mean
   taking the old one apart a card at a time. It sits directly under the deck's
   cards, and the question it raises wears the same frame as the restore
   control's: the first press asks, the second empties, so a stray press costs
   nothing. */
.deck-clear {
  display: grid;
  gap: 0.6rem;
  width: 100%;
  min-width: 0;
}

.deck-clear-confirm {
  display: grid;
  gap: 0.6rem;
  width: 100%;
  min-width: 0;
  padding: 0.85rem 1rem 0.95rem;
  border: 1px solid var(--edge);
  border-left: 3px solid var(--foil);
  border-radius: var(--radius-control);
}

.deck-clear-confirm[hidden] {
  display: none;
}

.deck-clear-confirm-text {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.5;
}

.deck-clear-confirm-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* The deck's cost curve, under its cards: the shape of the deck as one row per
   energy cost it holds, so building toward a curve is visible while building. */
.deck-curve {
  display: grid;
  gap: 0.35rem;
  padding-top: 0.15rem;
  border-top: 1px solid var(--edge);
}

/* Without this the block keeps its `display: grid` and refuses to fold, so an
   empty deck would still show the totals' labels and the curve's heading with
   nothing under them. */
.deck-curve[hidden] {
  display: none;
}

/* The deck's strength beside its curve: total attack and total health as a
   labelled row each, so what a change to the deck did to the deck is read the
   moment the deck changes. */
.deck-totals {
  display: grid;
  gap: 0.2rem;
  margin: 0;
  padding-top: 0.45rem;
}

.deck-total-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

.deck-total-label {
  color: var(--ink-muted);
  font-weight: 600;
}

.deck-total-value {
  margin: 0;
  color: var(--foil);
  font-weight: 700;
}

.deck-curve-heading {
  margin: 0;
  padding-top: 0.45rem;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* One row per cost, the cost on the left and its count on the right, so the
   curve's rows line up and its numbers can be compared down the column. */
.deck-curve-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.2rem;
}

.deck-curve-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

.deck-curve-cost {
  color: var(--ink);
  font-weight: 600;
}

.deck-curve-count {
  color: var(--foil);
}

/* The home screen's one-line deck summary, beside the numbers it restates. */
.deck-summary {
  margin: 0;
  color: var(--ink-muted);
  text-wrap: pretty;
}

/* The home screen's deck as its cards: the same faces the deck panel draws, at
   the compact size the opponent picker uses — small grid, tighter name and
   numbers — so a ten-card deck reads as a short hand rather than a list of
   names the player has to read. */
.home-deck-list {
  grid-template-columns: repeat(auto-fill, minmax(4.4rem, 1fr));
  gap: 0.3rem;
}

.home-deck-list .deck-card {
  gap: 0.15rem;
  padding: 0.25rem 0.3rem 0.35rem;
  background-color: rgba(34, 42, 69, 0.55);
}

.home-deck-list .deck-card-name {
  font-size: 0.72rem;
  line-height: 1.2;
}

.home-deck-list .deck-card-stats {
  font-size: 0.7rem;
}

/* The card the next win will add, on the home screen under the numbers it is the
   goal of: the label, then the card itself, so it is recognised at a glance and
   the advance to the following card after a win is visible. It stays in the same
   column as the standings rather than spanning the card, so the action rail on a
   wide window is left where it is. */
.next-reward {
  grid-column: 1;
  display: grid;
  gap: 0.45rem;
}

.next-reward-heading {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.next-reward .award-card {
  margin: 0;
}

.next-reward-note {
  margin: 0;
  color: var(--ink-muted);
  text-wrap: pretty;
}

/* The match left behind, offered on the home screen: a board's worth of labelled
   numbers and the one control that picks it back up, framed like the notes so it
   reads as news about the player's own game rather than a second action rail. */
.continue-match {
  grid-column: 1 / -1;
  display: grid;
  gap: 0.9rem;
  padding: 0.9rem 1.1rem 1.1rem;
  border: 1px solid var(--edge);
  border-left: 3px solid var(--foil);
  border-radius: var(--radius-control);
  background-color: rgba(168, 177, 212, 0.05);
}

.continue-match[hidden] {
  display: none;
}

.continue-match-heading {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

.continue-match .standings {
  gap: clamp(1rem, 2.5vw, 2rem);
}

/* Quieter than the standings above: this is a reminder of a board, not the
   headline numbers of the whole game. */
.continue-match .standing-value {
  font-size: clamp(1.1rem, 0.95rem + 0.6vw, 1.5rem);
}

.continue-match .btn {
  justify-self: start;
}

/* What just happened to the deck. The live region is always in the page — never
   display: none, or a screen reader may not announce the change — but only
   draws its frame once it has something to say. */
.deck-message {
  margin: 0;
  font-size: 0.9rem;
}

.deck-message:not(:empty) {
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--edge);
  border-left: 3px solid var(--foil);
  border-radius: var(--radius-control);
}

/* Shown only when the browser refuses to store anything: a statement of what is
   true, in the same frame as the cards it is about. */
.storage-note {
  margin: 0;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--edge);
  border-left: 3px solid var(--foil);
  border-radius: var(--radius-control);
  color: var(--ink);
  font-size: 0.9rem;
}

/* The save code and the restore controls, folded behind this heading and button
   so the deck a player came to work on is what the Collection screen opens on.
   The collapsed section is just the head; the body holds both panels. */
.save-restore {
  display: grid;
  gap: 0.3rem;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-control);
  background-color: rgba(168, 177, 212, 0.05);
}

.save-restore-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.save-restore-heading {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Sized quieter than the rail's big actions, but .btn's own min-height is what
   still holds the thumb's 44px target — only the padding gives way. */
.btn.save-restore-toggle {
  padding: 0.3rem 0.7rem;
  font-size: 0.78rem;
}

.save-restore-body {
  display: grid;
  gap: 0.75rem;
}

/* Without this the body keeps its `display: grid` and refuses to fold. */
.save-restore-body[hidden] {
  display: none;
}

/* The whole game as one save code: a heading, a line about what it is for, and
   the one control that reveals it. The panel and the code box only draw once
   the code is on screen. */
.save-code {
  display: grid;
  gap: 0.6rem;
  justify-items: start;
}

.save-code-heading {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* The revealed code: a labelled box a player can read, select by hand or copy
   in one press, beside the button that copies it. */
.save-code-panel {
  display: grid;
  gap: 0.5rem;
  width: 100%;
  min-width: 0;
}

.save-code-panel[hidden] {
  display: none;
}

.save-code-label {
  color: var(--ink-muted);
  font-size: 0.9rem;
  letter-spacing: 0.02em;
}

/* Monospace, because a save code is a string to copy rather than prose to read,
   and wrapping, because a phone has no room for one long line — the whole code
   must be reachable rather than scrolled off the edge. */
.save-code-text {
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  padding: 0.6rem 0.7rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-control);
  background: var(--card);
  color: var(--ink);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  resize: vertical;
}

/* The example the paste box shows is worth reading, so it is drawn in the page's
   own muted ink rather than the browser's default grey, which is too faint on
   the card to be legible. */
.save-code-text::placeholder {
  color: var(--ink-muted);
  opacity: 1;
}

/* The words that say whether the code is on screen or was copied, in the same
   live-region frame as the deck message. Always in the page, never displayed
   away, so a screen reader hears the change. */
.save-code-status {
  margin: 0;
  font-size: 0.9rem;
}

.save-code-status:not(:empty) {
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--edge);
  border-left: 3px solid var(--foil);
  border-radius: var(--radius-control);
}

/* The question a pasted save code raises, and the two answers beside it. It is
   drawn only while an answer is waited on, so the restore control is one button
   until the player has a decision to make. */
.restore-confirm {
  display: grid;
  gap: 0.6rem;
  width: 100%;
  min-width: 0;
  padding: 0.85rem 1rem 0.95rem;
  border: 1px solid var(--edge);
  border-left: 3px solid var(--foil);
  border-radius: var(--radius-control);
}

.restore-confirm[hidden] {
  display: none;
}

.restore-confirm-text {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.5;
}

.restore-confirm-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

/* --------------------------------------------------------------------------
   Before a match: who it is against
   -------------------------------------------------------------------------- */

/* The note about the deck and the opponents to choose from stand where the board
   will be, and step aside together once a match is running. */
.match-intro {
  display: grid;
  gap: 0.9rem;
  align-content: start;
  min-width: 0;
  min-height: 0;
}

/* Without this the wrapper keeps its `display: grid` and stays on screen. */
.match-intro[hidden] {
  display: none;
}

/* The opponents on offer: one radio each, so the choice is one a keyboard and a
   screen reader make as easily as a thumb. */
.opponent-picker {
  display: grid;
  gap: 0.55rem;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.opponent-picker[hidden] {
  display: none;
}

.opponent-picker-legend {
  margin-bottom: 0.5rem;
  padding: 0;
  color: var(--ink-muted);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}

/* Options share a row when the width can hold more than one and stack when it
   cannot, so a phone in portrait reads one character per row and the first
   screen still holds all of them. The 11rem floor is the least a portrait and
   the words beside it need to sit next to each other without squeezing the look
   and the deck into a column of two words; the picker is given the whole width
   of the screen on a wide window (see #screen-play), where every opponent is laid
   in one row rather than wrapping to a second one and pushing Start match off the
   first screen. */
.opponent-options {
  display: grid;
  align-items: stretch;
  gap: 0.45rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
}

/* An option is the character being picked and the deck they bring: the label
   above, which is the radio, and the deck folded in under it. The label takes
   the room it needs and the fold is held at the foot, so the options in a row
   are the same height and their deck lines read as one line across the picker
   however differently the characters' words wrap. */
.opponent-option {
  display: grid;
  grid-template-rows: 1fr auto;
  border: 1px solid var(--edge);
  border-radius: var(--radius-control);
  background-color: rgba(168, 177, 212, 0.04);
  transition:
    border-color 160ms var(--ease-out),
    background-color 160ms var(--ease-out);
}

/* The character is the radio's label — its mark in its own accent, its name and
   the record against it, then the look it cuts and the kind of deck it brings —
   so pressing any part of this top half picks that opponent. The radio, the
   portrait and the name share the first row and the two description lines take
   the full width under them: at nine opponents across a laptop the column beside
   a portrait is too narrow to read a sentence in, and a sentence that wraps to
   six lines is what pushes Start match off the first screen. The padding is what
   the picker has to spare once each character carries a record line. */
.opponent-choice {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  align-items: center;
  gap: 0.1rem 0.45rem;
  padding: 0.2rem 0.45rem;
  cursor: pointer;
}

/* The three lines of the character's words are the label's own children, so the
   grid above can place each of them: the name beside the portrait, the look and
   the deck across the width below. */
.opponent-text {
  display: contents;
}

.opponent-option:hover {
  border-color: var(--edge-soft);
}

/* The chosen opponent is the one the Start match button will open the board
   against, so it is marked by more than a filled radio: the accent keyline, a
   lift in the surface and the portrait held at full strength. */
.opponent-option:has(.opponent-radio:checked) {
  border-color: var(--opponent-accent);
  background-color: rgba(168, 177, 212, 0.1);
}

/* The radio is the control, but the label is what is being picked, so the ring
   goes round the whole option rather than round the dot alone. */
.opponent-option:has(.opponent-radio:focus-visible) {
  outline: 2px solid var(--foil);
  outline-offset: 2px;
}

.opponent-radio {
  grid-column: 1;
  grid-row: 1;
  width: 1.1rem;
  height: 1.1rem;
  margin: 0;
  accent-color: var(--opponent-accent);
}

/* The portrait of each option is the character itself, drawn in that opponent's
   accent, the one detail cut into it in the ink that prints on foil. Just under
   two rem is big enough to know a face at a glance, and small enough that every
   opponent still stands in one row on a laptop window. It sits in the first row
   beside the radio and the name. */
.opponent-portrait {
  grid-column: 2;
  grid-row: 1;
  width: 1.9rem;
  height: 1.9rem;
  opacity: 0.8;
}

.opponent-option:has(.opponent-radio:checked) .opponent-portrait {
  opacity: 1;
}

.portrait-body {
  fill: var(--opponent-accent);
}

.portrait-accent {
  fill: var(--on-foil);
}

.opponent-name {
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
}

/* The character's name and how the player has fared against it share a row while
   the width allows it, and the record takes its own line under the name when it
   does not — so a narrow option loses nothing and a phone keeps the picker's
   height. It sits in the grid's first row, beside the portrait. */
.opponent-head {
  grid-column: 3;
  grid-row: 1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0 0.5rem;
}

/* The look the character cuts and the kind of deck it brings each take the whole
   width of the option, in the rows under the radio, the portrait and the name —
   the one place in a nine-across picker where a sentence has room to read. */
.opponent-look,
.opponent-deck {
  grid-column: 1 / -1;
  color: var(--ink-muted);
  font-size: 0.72rem;
  line-height: 1.15;
}

.opponent-look {
  grid-row: 2;
}

.opponent-deck {
  grid-row: 3;
  color: var(--ink);
}

/* How the player has fared against this opponent: a short record beside the name
   and above the deck they bring, in the same muted weight as the look, so it
   reads as a record rather than as part of the character's description. An
   opponent already beaten is marked by the word "Beaten" in the line and by the
   lift in weight here — never by colour alone. */
.opponent-record {
  color: var(--ink-muted);
  font-size: 0.72rem;
  line-height: 1.2;
  font-variant-numeric: tabular-nums;
}

.opponent-record-beaten {
  color: var(--ink);
  font-weight: 600;
}

/* The deck an opponent brings, folded under the character: one line saying how
   many cards it holds, and the faces a match against them is dealt. Ten faces
   under every opponent would push the picker and Start match off the first
   screen, so the deck waits one press away, in the option that promises it. */
.opponent-deck-fold {
  border-top: 1px solid var(--edge-soft);
  padding: 0.1rem 0.45rem 0.15rem;
}

.opponent-deck-toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  min-height: 1.4rem;
  padding: 0 0.1rem;
  color: var(--ink-muted);
  font-size: 0.72rem;
  letter-spacing: 0.01em;
  cursor: pointer;
  list-style: none;
  transition: color 160ms var(--ease-out);
}

/* The browser's own triangle is drawn in a colour from no palette on this page,
   and the caret below takes its place. */
.opponent-deck-toggle::-webkit-details-marker {
  display: none;
}

.opponent-deck-toggle:hover {
  color: var(--ink);
}

/* A caret that turns as the deck opens, so the press is answered before the
   faces under it have been read. */
.opponent-deck-toggle::after {
  content: "";
  width: 0.42rem;
  height: 0.42rem;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-0.1rem, -0.1rem);
  transition: transform 160ms var(--ease-out);
}

.opponent-deck-fold[open] .opponent-deck-toggle::after {
  transform: rotate(-135deg) translate(-0.1rem, -0.1rem);
}

/* The opponent's ten cards, sized to sit inside their option: the same faces
   the deck screen draws — picture, name, three numbers — small enough that a
   whole deck reads as a hand rather than as a second deck screen. */
.opponent-deck-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(4.4rem, 1fr));
  gap: 0.3rem;
  margin: 0.1rem 0 0;
  padding: 0;
  list-style: none;
}

.opponent-deck-cards .deck-card {
  gap: 0.15rem;
  padding: 0.25rem 0.3rem 0.35rem;
  background-color: rgba(34, 42, 69, 0.55);
}

.opponent-deck-cards .deck-card-name {
  font-size: 0.72rem;
  line-height: 1.2;
}

.opponent-deck-cards .deck-card-stats {
  font-size: 0.7rem;
}

/* What an opponent's deck is built around, under the hand it brings: the same
   labelled counts the player's own deck panel prints, in the panel's own words,
   with a row only for an ability that deck actually carries — so how much of it
   sweeps or mends is read as numbers rather than inferred from ten faces. */
.opponent-deck-totals {
  margin-top: 0.3rem;
  padding-top: 0.3rem;
  border-top: 1px solid var(--edge-soft);
}

.opponent-deck-totals .deck-total-row {
  font-size: 0.72rem;
  line-height: 1.25;
}

/* The one card in an opponent's deck a first win earns: the fact is written on
   the card itself, so a player choosing who to face sees what is on offer and
   which card stands for how that character plays — and a screen reader or an
   agent reads the same words rather than a colour. Quiet enough to sit under
   the card's numbers without shouting over the rest of the hand. */
.opponent-deck-cards .signature-note {
  padding: 0.05rem 0.3rem;
  border: 1px solid rgba(242, 193, 78, 0.5);
  border-radius: 999px;
  color: var(--foil);
  font-size: 0.62rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: 0.01em;
}

/* The how-a-match-works strip: the four things a newcomer cannot yet read for
   themselves, said in four short labelled lines that stand before the picker
   and the Start match button. It folds away behind one control, so a returning
   player gets the space back. */
.match-rules {
  display: grid;
  gap: 0.3rem;
  padding: 0.25rem 0.6rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-control);
  background-color: rgba(168, 177, 212, 0.05);
}

/* Without this the strip keeps its `display: grid` and refuses to fold. */
.match-rules[hidden] {
  display: none;
}

.match-rules-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.match-rules-heading {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Sized quieter than the rail's big actions, but .btn's own min-height is what
   still holds the thumb's 44px target — only the padding gives way. */
.btn.match-rules-toggle {
  padding: 0.3rem 0.7rem;
  font-size: 0.78rem;
}

.match-rules-points {
  display: grid;
  gap: 0.1rem;
  margin: 0;
}

.match-rules-points[hidden] {
  display: none;
}

/* The label and its line share a row as two boxes of their own, so four points
   cost four rows rather than eight. `fit-content` gives a short label the width
   its own words need — the line starts right after it, as it always has — and
   caps the long fourth label at a share of the row, so the label that wraps and
   the line beside it are two boxes that share no pixels. The cap is what keeps
   the picker and the Start match button on the first screen with all four
   points open: a column as wide as the whole fourth label on one line squeezes
   the line beside it into twice the height. The leading is the tightest this
   size reads at, for the same reason. */
.match-rule {
  display: grid;
  grid-template-columns: fit-content(24%) minmax(0, 1fr);
  column-gap: 0.35rem;
  align-items: start;
  font-size: 0.76rem;
  line-height: 1.2;
}

.match-rule-label,
.match-rule-text {
  margin: 0;
}

.match-rule-label {
  font-weight: 700;
}

.match-rule-text {
  color: var(--ink-muted);
}

/* --------------------------------------------------------------------------
   The match
   -------------------------------------------------------------------------- */

/* The Play screen is the board: one width, its own internal grid below, rather
   than the numbers-left/actions-right split the other screens use. */
#screen-play > * {
  grid-column: 1 / -1;
}

.match-board {
  display: grid;
  gap: clamp(0.9rem, 2vw, 1.4rem);
}

/* Without this a hidden board keeps its `display: grid` and stays on screen. */
.match-board[hidden] {
  display: none;
}

/* Both heroes, the numbers a turn turns on, and whose turn it is, in one row
   above the table. */
.match-standings {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(1.1rem, 2.5vw, 2.25rem);
  margin: 0;
  padding-bottom: clamp(0.6rem, 1.4vw, 0.9rem);
  border-bottom: 1px solid var(--edge-soft);
}

.match-standings .standing-value {
  font-size: clamp(1.4rem, 1.1rem + 1vw, 2rem);
}

/* The ready-to-strike line carries a phrase where the rest of the row carries a
   single number, so it reads a size down and keeps the figures beside it from
   stretching the row. */
.match-standings .standing-ready .standing-value {
  font-size: clamp(0.95rem, 0.8rem + 0.5vw, 1.25rem);
  line-height: 1.3;
}

/* A standing that is not being shown must give up its place in the row: it is
   laid out with `display: flex`, which outweighs what `hidden` does on its own,
   so the turn would stay on the board once the match is over. */
.match-standings .standing[hidden] {
  display: none;
}

/* The opponent's standing is led by their portrait, so the character the picker
   showed is beside the name and health it already prints for the whole match.
   The drawing sits left of the label and the number, centred on the pair, and
   the slot is empty outside a match. */
.standing-opponent {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  column-gap: 0.6rem;
  align-items: center;
}

.match-opponent-portrait {
  grid-area: 1 / 1 / 3 / 2;
  display: grid;
  place-items: center;
}

/* A portrait that is not being shown must give up its place in the grid, which
   outranks what `hidden` does on its own. */
.match-opponent-portrait[hidden] {
  display: none;
}

/* The board's portrait is the character itself, at full strength: nothing here
   chooses between opponents, so it is not dimmed the way the picker's are. */
.match-opponent-portrait .opponent-portrait {
  width: 2.75rem;
  height: 2.75rem;
  opacity: 1;
}

.standing-opponent .standing-label {
  grid-area: 1 / 2 / 2 / 3;
  align-self: end;
}

.standing-opponent .standing-value {
  grid-area: 2 / 2 / 3 / 3;
  align-self: start;
}

/* What the faced opponent's deck is built to do, in the picker's own sentence:
   a caption that takes the room the row of numbers has left rather than a line
   of its own, so it is read beside the health it explains without costing the
   board the height a table or the log would have to give up. It is a sentence,
   not a number, so it reads in the ink of the text around it at text size. */
.match-standings .standing-opponent-deck {
  flex: 1 1 16rem;
  min-width: 0;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 0.4rem;
  row-gap: 0;
}

.match-standings .standing-opponent-deck .standing-value {
  color: var(--ink);
  font-size: 0.85rem;
  font-weight: 500;
  line-height: 1.3;
}

/* The match as a result. It lands under the health row so the outcome is read
   where the player last looked, and only draws its frame once a hero has
   fallen — the one moment in a match worth marking. */
.match-result {
  display: grid;
  justify-items: start;
  gap: 0.45rem;
  padding: clamp(0.85rem, 1.8vw, 1.15rem) clamp(1rem, 2.4vw, 1.4rem);
  border: 1px solid var(--foil);
  border-radius: var(--radius-control);
  background-color: rgba(242, 193, 78, 0.09);
  animation: result-land 260ms var(--ease-out) both;
}

/* Without this a hidden result keeps its `display: grid` and stays on screen. */
.match-result[hidden] {
  display: none;
}

@keyframes result-land {
  from {
    opacity: 0;
    transform: scale(0.98);
  }
}

.result-heading {
  margin: 0;
  font-size: clamp(1.35rem, 1.1rem + 1.2vw, 1.85rem);
  letter-spacing: -0.015em;
  text-wrap: balance;
}

.result-health {
  margin: 0;
  color: var(--ink);
  text-wrap: pretty;
}

/* The one win the result marks as a first against that opponent: said under the
   heading, before the numbers, because it is the payoff rather than a count. */
.result-first-win {
  margin: 0;
  color: var(--foil);
  font-weight: 600;
  text-wrap: pretty;
}

/* What the result did to the collection — the reason a player comes back, so it
   sits with the outcome and reads at the same weight as the rest of the card. */
.result-reward {
  margin: 0;
  color: var(--ink);
  text-wrap: pretty;
}

.result-reward:empty {
  display: none;
}

/* The label above the earned card, so the picture is read as the reward rather
   than as another card in play. */
.award-card-label {
  display: block;
  color: var(--ink-muted);
}

/* The card a win earned, shown as the card itself: its picture beside its name
   and three numbers, at a size big enough to recognise and small enough to sit
   in the result. */
.award-card {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin: 0.4rem 0 0.1rem;
}

.award-card .card-art {
  width: clamp(5rem, 22vw, 7rem);
  flex: none;
}

.award-card-caption {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  min-width: 0;
}

.award-card-name {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.award-card-stats {
  color: var(--foil);
  font-variant-numeric: tabular-nums;
}

.result-standings {
  margin-top: 0.15rem;
}

.result-standings .standing-value {
  font-size: clamp(1.2rem, 1rem + 0.8vw, 1.6rem);
}

/* The result's way back into a match, sized like the board's other controls. */
.match-result .btn {
  margin-top: 0.2rem;
}

.match-side,
.match-log-wrap {
  display: grid;
  gap: 0.5rem;
  padding: 0.9rem 1rem 1rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-control);
  background-color: rgba(15, 19, 34, 0.4);
}

/* The hand goes when a hero falls, and `display: grid` would otherwise keep its
   empty frame standing over the result. */
.match-side[hidden],
.match-log-wrap[hidden] {
  display: none;
}

.match-heading {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

/* Each table sits on its own lit playing surface: a raised edge, a soft sheen
   from above and a faint weave, so a card is laid on a surface rather than
   floating in the panel around it. The surface does not change with its
   contents — an empty one is the same table, waiting. */
.match-surface {
  position: relative;
  padding: 0.6rem;
  border: 1px solid var(--edge-soft);
  border-radius: var(--radius-control);
  background-color: rgba(27, 34, 64, 0.72);
  background-image:
    radial-gradient(120% 90% at 50% 0%, rgba(255, 255, 255, 0.06), transparent 58%),
    repeating-linear-gradient(135deg, rgba(255, 255, 255, 0.018) 0 5px, transparent 5px 10px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.05),
    inset 0 14px 26px -20px rgba(2, 4, 12, 0.95);
}

/* An empty table: the sentence still reads as text, and above it a dashed card
   slot waits on the surface, so the table looks half-played rather than blank. */
.match-table .match-empty {
  grid-column: 1 / -1;
  display: grid;
  justify-items: center;
  gap: 0.45rem;
  padding: 0.9rem 0.75rem;
  color: var(--ink-muted);
}

.match-empty-slot {
  width: 4.25rem;
  aspect-ratio: 96 / 64;
  border: 1px dashed var(--edge-soft);
  border-radius: calc(var(--radius-control) - 4px);
  background-color: rgba(15, 19, 34, 0.35);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
}

.match-empty-text {
  font-size: 0.9rem;
}

/* A pile of face-down cards — the opponent's hand and the deck still to draw.
   The label stays quiet; the backs are the picture of the count printed beside
   them on the board. */
.match-pile {
  display: grid;
  gap: 0.3rem;
  justify-items: start;
}

.match-pile-label {
  margin: 0;
  color: var(--ink-muted);
  font-size: 0.78rem;
}

.card-pile {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.28rem;
}

.card-pile-card {
  display: block;
  line-height: 0;
}

.card-pile-empty {
  color: var(--ink-muted);
  font-size: 0.8rem;
  line-height: 1.4;
}

/* The back of a card, drawn by cardart.js in the same frame as a face. */
.card-pile-card .card-back {
  display: block;
  width: 3rem;
  border-radius: 0.3rem;
  box-shadow: 0 3px 6px -4px rgba(2, 4, 12, 0.9);
}

/* The deck is a stack seen from the side: each back overlaps the one before it
   and steps a little to the right, so the pile's height follows how many cards
   are left and it visibly shrinks as they are drawn. */
.card-pile-stack {
  flex-direction: column;
  align-items: flex-start;
}

.card-pile-stack .card-pile-card {
  transform: translateX(calc(var(--i, 0) * 3px));
}

.card-pile-stack .card-pile-card + .card-pile-card {
  margin-top: -1.35rem;
}

.card-pile-stack .card-back {
  width: 2.6rem;
}

/* Every card in a match — in hand and on either table — is the same
   picture-first face the collection uses: one shared face width, so the same
   card is the same size wherever it appears and a creature is known by sight
   rather than by a thumbnail beside its name. */
.match-hand,
.match-table {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, var(--match-face));
  gap: 0.5rem;
}

/* The face itself: the picture on top, then its name and cost/attack/health as
   a caption, then the control that plays or attacks with it. */
.match-card {
  display: grid;
  align-content: start;
  gap: 0.4rem;
  min-width: 0;
  padding: 0.5rem 0.45rem 0.55rem;
  border: 1px solid var(--edge);
  border-radius: var(--radius-control);
  background-color: rgba(34, 42, 69, 0.7);
}

/* The shared .card-art rule already sizes a picture to the full width at the
   drawing's own 96/64 ratio, so a face's picture is simply the top of the card,
   the same drawing as the collection's at the same aspect. */
.match-card .card-art {
  width: 100%;
}

.match-card-head {
  display: grid;
  gap: 0.05rem;
  min-width: 0;
}

.match-card-name {
  font-weight: 700;
  overflow-wrap: anywhere;
}

.match-card-stats {
  color: var(--foil);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
}

/* The one target an ending turn is about to hit, said in words: the card at the
   front of the opponent's table, or their hero once that table is empty. It is a
   label, not a control — ending the turn is the move, and where the blows land
   follows from it — so it takes no pointer and never reads as something
   pressable. Its foil keyline is the accent the numbers wear, so a player
   scanning the board finds where the next blow lands without reading the log. */
.match-strike-mark {
  /* A chip rather than a full-width bar, wherever it lands: on a card's face or
     under the empty battlefield. */
  width: fit-content;
  padding: 0.1rem 0.5rem;
  border: 1px solid rgba(242, 193, 78, 0.5);
  border-radius: 999px;
  color: var(--foil);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-wrap: pretty;
}

/* On a card the mark heads the face while it reads last, after the card's own
   name and numbers: a table surface is a fixed height on a wide window, and a
   tall card's lower edge is the part that scrolls out of it, so the wording must
   not be the part a player has to go looking for. */
.match-card .match-strike-mark {
  order: -1;
}

/* Under an emptied battlefield it follows the cards that stood there. */
.match-surface > .match-strike-mark {
  margin: 0.6rem 0 0;
}

/* The card the mark names carries the same keyline, so the chip and the card it
   sits on read as one mark rather than a label dropped on any face. */
.match-card.is-end-turn-target {
  border-color: rgba(242, 193, 78, 0.5);
}

/* A blow landing on the table: the card that struck lifts into it, the card it
   hit shakes under the blow, and a card worn out fades and drops off the row.
   All three are decoration — the health figure and the log carry the same fact
   to a screen reader and an agent — and they are turned off wholesale for a
   visitor who asked for less motion. */
.match-card.is-striking {
  animation: card-strike 340ms var(--ease-out);
}

.match-card.is-hit {
  animation: card-hit 340ms var(--ease-out);
}

/* A card that fired a when-played ability as it landed — it drew a card or
   mended its hero — wears a foil ring while the moment lasts, so the effect is
   seen on the table and not only read in the log. The ring is the mark; the
   lift is only the flourish on top of it, so a visitor who asked for less
   motion still sees which card acted. */
.match-card.is-ability {
  border-color: var(--foil);
  box-shadow:
    0 0 0 2px rgba(242, 193, 78, 0.5),
    0 0 14px 2px rgba(242, 193, 78, 0.3);
  animation: card-ability 420ms var(--ease-out);
}

/* A card the opponent played on its last turn: a quiet chip naming it, cleared
   on the next draw of the board. It is a label, not a control — it takes no
   pointer — and its words are real text, so a screen reader hears which cards
   are new without the log being read back. The muted ink sets it apart from the
   foil of a blow, so "just played" reads as news of the board rather than as
   another strike; nothing animates it, so a visitor who asked for less motion
   still sees it whole. */
.match-just-played-mark {
  width: fit-content;
  padding: 0.1rem 0.5rem;
  border: 1px solid rgba(168, 177, 212, 0.5);
  border-radius: 999px;
  color: var(--ink-muted);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-wrap: pretty;
}

/* On a card the mark heads the face while it reads last, the same as the
   end-of-turn target chip, so a fixed-height table surface cannot scroll it out
   of sight. */
.match-card .match-just-played-mark {
  order: -1;
}

/* The card it sits on wears the same muted keyline, so the chip and the card
   read as one mark rather than a label dropped on any face. */
.match-card.is-just-played {
  border-color: rgba(168, 177, 212, 0.55);
}

/* The card that has just left the table, still shown at the spot it stood. It
   takes no pointer and carries no card identity, and the log is what names the
   fall, so the marker never announces the blow a second time. */
.match-card-falling {
  border-style: dashed;
  background-color: rgba(34, 42, 69, 0.4);
  pointer-events: none;
  animation: card-fall 420ms var(--ease-out) both;
}

.match-card-falling-caption {
  color: var(--ink-muted);
  font-size: 0.8rem;
}

@keyframes card-strike {
  0% {
    transform: none;
  }
  35% {
    transform: translateY(-6px) scale(1.02);
  }
  100% {
    transform: none;
  }
}

@keyframes card-hit {
  0%,
  100% {
    transform: none;
  }
  20% {
    transform: translateX(-3px);
  }
  40% {
    transform: translateX(3px);
  }
  60% {
    transform: translateX(-2px);
  }
  80% {
    transform: translateX(2px);
  }
}

/* A smaller lift than a strike's, so an ability reads as its own moment rather
   than as another blow. */
@keyframes card-ability {
  0% {
    transform: none;
  }
  40% {
    transform: translateY(-4px) scale(1.02);
  }
  100% {
    transform: none;
  }
}

@keyframes card-fall {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0.3;
    transform: translateY(10px) rotate(3deg);
  }
}

/* The control that plays a hand card, or attacks with a table card, sits under
   the face at the face's own width rather than beside it. */
.match-card-playable .match-play,
.match-card-action .btn {
  width: 100%;
}

/* A card the player cannot afford yet: still pressable, quieter to look at. */
.match-card-playable.is-unaffordable .match-play {
  border-color: var(--edge-soft);
  color: var(--ink-muted);
  background-color: rgba(168, 177, 212, 0.03);
}

/* Sized like .deck-add, and scoped under .btn for the same reason: at equal
   specificity .btn's padding and font would win wherever it lands in the sheet. */
.btn.match-play {
  padding: 0.45rem 0.8rem;
  font-size: 0.85rem;
}

/* The battle controls read quieter than the rail's own actions, the same way a
   hand card's Play does, so a table of many cards is not a wall of loud buttons.
   Their labels wrap rather than push past the face they sit under. */
.btn.match-attack-select,
.btn.match-attack-target,
.btn.match-attack-hero {
  padding: 0.45rem 0.8rem;
  font-size: 0.85rem;
}

/* The picked card's control is filled, so the choice is read without relying on
   colour alone: its aria-pressed state carries the same fact to a screen reader. */
.match-attack-select[aria-pressed="true"] {
  background-color: var(--foil);
  border-color: var(--foil);
  color: var(--on-foil);
}

/* A card that has already struck this turn is quieter, but stays pressable so
   pressing it can say why it cannot strike again. */
.match-card.is-spent .match-attack-select {
  border-color: var(--edge-soft);
  color: var(--ink-muted);
  background-color: rgba(168, 177, 212, 0.03);
}

/* The lone control that strikes at the hero when the opponent's table is empty,
   standing where the opponent's cards would be. */
.match-attack-hero-item {
  display: inline-flex;
  border-style: dashed;
}

.match-empty {
  color: var(--ink-muted);
  font-size: 0.9rem;
}

/* The log grows with the match; it scrolls in place rather than pushing the
   table off the screen, and keeps its own scroll contained. */
.match-log {
  margin: 0;
  padding-left: 1.4rem;
  display: grid;
  gap: 0.2rem;
  max-height: 15rem;
  overflow-y: auto;
  overscroll-behavior: contain;
}

.match-log-line {
  color: var(--ink);
  text-wrap: pretty;
}

.match-log-line::marker {
  color: var(--ink-muted);
}

/* What just happened in the match. The live region is always in the page, only
   drawing its frame once it has something to say. It rides in the match's action
   rail and takes a line of its own there, so the status and the controls are read
   as one bar. */
.match-message {
  margin: 0;
  flex-basis: 100%;
  font-size: 0.9rem;
}

.match-message:not(:empty) {
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--edge);
  border-left: 3px solid var(--foil);
  border-radius: var(--radius-control);
}

/* --------------------------------------------------------------------------
   A match on a wide window fits one window
   -------------------------------------------------------------------------- */

/* The whole match — the health row, the hand, both tables, the log and the
   controls — has to be playable without scrolling, the way it already is on a
   phone, because End turn is the one control that moves the game on. So while a
   match is open the card gives its own chrome back to the table and is sized to
   the window, and the panels that grow without bound (the hand, both tables, the
   log) take the height that is left and scroll inside themselves. Nothing is
   drawn differently; it is the same board in a tighter frame. */
@media (min-width: 860px) {
  /* One row of exactly the window's height, so the card's own height is a
     definite one and the board below can be sized from what is left. */
  body:has(#screen-play:not([hidden]) #match-board:not([hidden])) {
    height: 100dvh;
    padding: 0.9rem;
    grid-template-rows: minmax(0, 1fr);
  }

  /* A definite height, not a cap: grid tracks are sized from it, which is what
     lets the board below take exactly the room the window has left. */
  body:has(#screen-play:not([hidden]) #match-board:not([hidden])) .card {
    display: grid;
    grid-template-rows: auto auto minmax(0, 1fr);
    height: 100%;
    padding: clamp(0.9rem, 1.6vw, 1.5rem);
  }

  /* The nameplate and the rules are how the table introduces itself, not part of
     a match: during one they step back to a quiet line each. */
  body:has(#screen-play:not([hidden]) #match-board:not([hidden])) .wordmark {
    font-size: clamp(1.5rem, 1rem + 1.2vw, 2.1rem);
  }

  body:has(#screen-play:not([hidden]) #match-board:not([hidden])) .rules {
    padding-block: 0.35rem 0.5rem;
    font-size: 0.9rem;
  }

  /* The board on the left, the match's controls on a rail of their own beside
     it, so End turn is never below what a player can see. */
  #screen-play {
    grid-template-columns: minmax(0, 1fr) 15rem;
    grid-template-rows: auto minmax(0, 1fr);
    align-items: stretch;
    column-gap: clamp(0.6rem, 1.2vw, 1rem);
    row-gap: 0.6rem;
    min-height: 0;
  }

  #screen-play > #play-heading {
    grid-row: 1;
    grid-column: 1 / -1;
  }

  /* Before a match the note stands where the board will be. */
  #screen-play > #match-intro,
  #screen-play > .match-board {
    grid-row: 2;
    grid-column: 1;
    min-height: 0;
  }

  /* The choice takes the whole width for a row of its own, rather than the
     left column the note uses: every opponent then stands in one line however
     many the game offers, so the picker keeps its height and Start match keeps
     the first screen. */
  #screen-play > #opponent-picker {
    grid-row: 3;
    grid-column: 1 / -1;
  }

  /* Every opponent stands in one row however many the game offers, rather than
     wrapping to a second one and pushing Start match off the first screen. A
     fixed column flow makes each option an equal share of the row instead of a
     11rem track per character, and the nine of them fit the first screen only
     if each stays short: a tighter gap, padding and description size keep the
     wrapped look and deck lines from growing the option past the fold. The
     phone's single-column stack is untouched, since this only applies where the
     wide layout begins. */
  .opponent-options {
    grid-template-columns: none;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    gap: 0.2rem;
  }

  .opponent-choice {
    gap: 0.05rem 0.35rem;
    padding: 0.1rem 0.3rem;
  }

  .opponent-look,
  .opponent-deck {
    font-size: 0.7rem;
    line-height: 1.1;
  }

  /* The folded deck sits on the same inset as the words above it, so the option
     reads as one column rather than an indented footnote. */
  .opponent-deck-fold {
    padding-inline: 0.3rem;
  }

  /* A finished match keeps the picker above it — the next one can be against
     someone else — so the board takes the row below the choice instead of
     sharing its cell with it. The rows only grow to hold the picker while it
     is on screen; once a match is running the picker and the note are gone and
     the board takes their place. */
  #screen-play:has(> #opponent-picker:not([hidden])) {
    grid-template-rows: auto auto auto minmax(0, 1fr);
  }

  #screen-play:has(> #opponent-picker:not([hidden])) > .match-board {
    grid-row: 4;
    /* The choice costs the board a row, so the board keeps a height its tables
       and log can be read at and the page scrolls rather than squashing them. */
    min-height: 26rem;
  }

  #screen-play > .actions {
    display: grid;
    grid-row: 2;
    grid-column: 2;
    grid-auto-rows: min-content;
    align-self: start;
    align-content: start;
    gap: 0.5rem;
  }

  /* The hand across the top, then your table, theirs and the log in a row:
     every part of a turn under one glance, in the order a turn is played. The
     hand is held to a share of the board rather than to its content, so a hand
     that wraps onto a second row cannot push both tables off the bottom. */
  #screen-play > .match-board {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1.15fr) minmax(0, 0.7fr);
    grid-template-rows: auto minmax(0, 40%) minmax(0, 1fr);
    align-items: stretch;
    gap: clamp(0.5rem, 0.9vw, 0.8rem);
  }

  /* A finished match is what is on the board: the outcome takes the height it
     needs to be read whole, and the idle tables below it give theirs up. */
  #screen-play > .match-board:has(.match-result:not([hidden])) {
    grid-template-rows: auto minmax(0, auto) minmax(0, 1fr);
  }

  /* The health row is a line of the board, not a band: tightened to the numbers
     a turn turns on so the hand and tables keep their room. */
  .match-board .match-standings {
    grid-area: 1 / 1 / 2 / -1;
    gap: clamp(0.9rem, 2vw, 1.6rem);
    padding-bottom: 0.45rem;
  }

  .match-board .match-standings .standing-value {
    font-size: clamp(1.15rem, 0.9rem + 0.7vw, 1.5rem);
  }

  .match-board .match-standings .standing-label {
    font-size: 0.8rem;
  }

  /* The deck sentence keeps its reading size in the tightened health row: it is
     a sentence, not a figure, so it is not scaled with the numbers beside it. */
  .match-board .match-standings .standing-opponent-deck .standing-value {
    font-size: 0.8rem;
  }

  /* The hand and the match's result share one slot: a match that has ended has
     no hand left to hold, so the outcome takes the room the cards had. Each is
     as tall as what it holds and no taller, so the room its slot is given is
     never empty space taken from the tables. */
  .match-board .match-hand-side,
  .match-board .match-result {
    grid-area: 2 / 1 / 3 / -1;
    align-self: start;
    max-height: 100%;
    min-height: 0;
  }

  .match-board .match-result {
    padding: clamp(0.7rem, 1.4vw, 0.95rem) clamp(0.8rem, 1.8vw, 1.1rem);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  .match-board #match-hand-side {
    grid-template-rows: auto minmax(0, 1fr);
  }

  /* A hand of more cards than the slot holds scrolls in its own row rather than
     climbing over the tables. */
  .match-board .match-hand {
    align-content: start;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* The player's deck sits under their battlefield, the opponent's hand above
     theirs; whichever side a pile is on, the surface is what takes the room. */
  .match-board #match-player-table-side {
    grid-area: 3 / 1 / 4 / 2;
    grid-template-rows: auto minmax(0, 1fr) auto;
  }

  .match-board #match-opponent-table-side {
    grid-area: 3 / 2 / 4 / 3;
    grid-template-rows: auto auto minmax(0, 1fr);
  }

  .match-board .match-log-wrap {
    grid-area: 3 / 3 / 4 / 4;
    grid-template-rows: auto minmax(0, 1fr);
  }

  /* A match's panels hold the room they are given rather than growing it: a
     panel's own content scrolls, so the page never does. Their own chrome is
     tightened here too, because every pixel of padding and heading is a pixel a
     table cannot show of the cards on it. */
  .match-board .match-side,
  .match-board .match-log-wrap {
    min-height: 0;
    gap: 0.35rem;
    padding: 0.5rem 0.6rem 0.55rem;
  }

  .match-board .match-heading {
    font-size: 0.9rem;
    line-height: 1.2;
  }

  /* A battlefield that holds more cards than its panel can show scrolls under
     the hand that laid them, instead of stretching the row. */
  .match-board .match-surface {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* The card itself is drawn a touch tighter in a match, so a full card face —
     picture, numbers and the control under them — fits one row of its table. */
  .match-board .match-card {
    gap: 0.35rem;
    padding: 0.45rem 0.4rem 0.5rem;
  }

  /* Face-down backs are smaller than a face, so a pile costs a table a strip of
     its panel rather than a card's height. */
  .match-board .card-pile-card .card-back {
    width: 2.2rem;
  }

  .match-board .card-pile-stack .card-pile-card + .card-pile-card {
    margin-top: -1.15rem;
  }

  /* The log fills the height its panel was given instead of a fixed height of
     its own, so a long match scrolls inside the panel rather than down the page. */
  .match-board .match-log {
    max-height: none;
    min-height: 0;
  }
}

/* --------------------------------------------------------------------------
   Actions
   -------------------------------------------------------------------------- */

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}

.btn {
  font: inherit;
  font-weight: 600;
  padding: 0.8rem 1.35rem;
  /* A control a thumb can hit: WCAG asks 44px at AAA, and a phone game is tapped
     repeatedly. This floor, not the padding, is what holds the tap target — the
     smaller component paddings below shrink the control no further than this. */
  min-height: 2.75rem;
  border: 1px solid transparent;
  border-radius: var(--radius-control);
  background-color: transparent;
  color: var(--ink);
  cursor: pointer;
  touch-action: manipulation;
  transition: background-color 140ms ease, border-color 140ms ease, transform 120ms var(--ease-out);
}

.btn:active {
  transform: scale(0.97);
}

/* Every control on the card shows where the keyboard is, in the card's own foil
   keyline — whether it is one of the game's buttons or a plain control the
   browser drew, so no control is ever focusable without showing it. */
:where(button, a[href], input, select, textarea, summary):focus-visible {
  outline: 2px solid var(--foil);
  outline-offset: 2px;
}

.btn-primary {
  background-color: var(--foil);
  border-color: var(--foil);
  color: var(--on-foil);
}

.btn-secondary {
  background-color: rgba(168, 177, 212, 0.06);
  border-color: var(--edge);
}

@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover {
    background-color: var(--foil-lit);
    border-color: var(--foil-lit);
  }

  .btn-secondary:hover {
    background-color: rgba(168, 177, 212, 0.14);
    border-color: #56638f;
  }
}

/* A phone gets the actions as full-width targets under the numbers, and the
   match's rail as a bar docked to the foot of the window so the health, the
   energy, the hand and the controls all read without scrolling. The card's own
   chrome is pulled in too: on a short phone every pixel above the board is a
   pixel the hand cannot use. */
@media (max-width: 719px) {
  body {
    padding: 0.5rem;
  }

  .card {
    padding: 0.9rem 0.9rem 1rem;
  }

  .wordmark {
    font-size: clamp(1.9rem, 1.4rem + 3vw, 2.4rem);
  }

  .rules {
    padding-block: 0.6rem 0.8rem;
    font-size: 0.95rem;
  }

  .actions {
    display: grid;
  }

  /* On a phone the continue offer's control is a full-width target like the
     screen's own actions, so a thumb does not have to find a smaller button. */
  .continue-match .btn {
    justify-self: stretch;
  }

  /* The note and the rules sit together above the picker on a phone, so their
     gap tightens a little: still grouped, but closer together. */
  .match-intro {
    gap: 0.6rem;
  }

  /* The fourth point now names a third kind of ability, so its line wraps once
     more on a phone. The strip's own gaps and padding tighten to absorb that
     line, so the picker and the Start match button still share the first
     screen with all four points open. */
  .match-rules {
    gap: 0.15rem;
    padding-top: 0.1rem;
    padding-bottom: 0.1rem;
  }

  .match-rules-points {
    gap: 0;
  }

  /* A phone's row is too narrow to hold the long fourth label beside its line
     and still read, so the two stack: the label on its own line, its line
     below it. Still one point per row, still four points. */
  .match-rule {
    grid-template-columns: 1fr;
    column-gap: 0;
  }

  /* The how-a-match-works strip and the opponent picker share the first screen,
     so on a phone the options tighten: the name, the record against the
     opponent, the look and the deck all still read, in fewer lines. */
  .opponent-picker-legend {
    margin-bottom: 0.15rem;
  }

  .opponent-options {
    gap: 0.2rem;
  }

  .opponent-choice {
    padding: 0.25rem 0.6rem;
    gap: 0.1rem 0.5rem;
  }

  /* The deck line under each character keeps its press and its thumb-sized
     height, and takes no other room, so the picker stays as short on a phone as
     one stacked option per character can be. */
  .opponent-deck-fold {
    padding: 0 0.6rem 0.15rem;
  }

  .opponent-deck-toggle {
    min-height: 1.4rem;
    font-size: 0.75rem;
  }

  .opponent-look,
  .opponent-deck,
  .opponent-record {
    font-size: 0.75rem;
    line-height: 1.25;
  }

  /* The bar covers the foot of the window, so the card keeps enough room under
     its last content for the log to be scrolled clear of it. */
  #screen-play {
    padding-bottom: calc(6rem + env(safe-area-inset-bottom));
    animation: screen-fade 200ms var(--ease-out) both;
  }

  #screen-play .actions {
    position: fixed;
    left: 0.5rem;
    right: 0.5rem;
    bottom: 0;
    z-index: 2;
    /* A grid rather than a flex wrap: the controls that a match shows at once
       always share one row, so the bar never grows a row taller (and never
       taller than the hand it sits over) when a long label is involved. The
       track is sized for the three controls a live match shows — End turn,
       Concede, Back to home — so they keep that row on the narrowest phone. */
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
    gap: 0.5rem;
    padding: 0.5rem 0.9rem calc(0.5rem + env(safe-area-inset-bottom));
    border: 1px solid var(--edge);
    border-bottom: 0;
    border-radius: var(--radius-control) var(--radius-control) 0 0;
    background-color: var(--card);
  }

  /* The status line rides its own row across the whole bar. */
  #screen-play .actions .match-message {
    grid-column: 1 / -1;
  }

  /* min-height still holds the 44px tap target; only the padding gives way, so
     the bar is as short as a control can be. */
  #screen-play .actions .btn {
    white-space: nowrap;
    padding: 0.35rem 0.8rem;
  }

  /* The question a Start match asks carries two full-sentence answers, too long
     for one row of the bar, so on a phone they stack as full-width targets. The
     bar is taller for it, and the card keeps room below so nothing is left
     behind the bar. */
  #screen-play[data-start-pending="true"] .actions {
    grid-template-columns: 1fr;
  }

  /* The question is the whole point of the choice, so it reads in full rather
     than being bounded to the row a refusal is. */
  #screen-play[data-start-pending="true"] .match-message:not(:empty) {
    max-height: none;
    overflow: visible;
  }

  #screen-play[data-start-pending="true"] {
    padding-bottom: calc(16rem + env(safe-area-inset-bottom));
  }

  /* A phone's rhythm, tighter than a wide window's: the hand has to clear the
     bar with room to spare. */
  .screen {
    gap: 0.6rem;
    padding-top: 0.5rem;
  }

  .match-board {
    gap: 0.6rem;
  }

  /* The numbers a turn turns on — including whose turn it is — in two compact
     columns with the label beside its value, so the health row costs a line or
     two rather than half the screen. */
  .match-standings {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.3rem 0.9rem;
    padding-bottom: 0.4rem;
  }

  .match-standings .standing {
    flex-direction: row;
    align-items: baseline;
    gap: 0.35rem;
  }

  /* The opponent's portrait shrinks with the row's numbers so it sits inside the
     standing rather than growing the compact health row by a line. */
  .match-standings .standing-opponent {
    align-items: center;
  }

  .match-standings .standing-opponent .opponent-portrait {
    width: 2.25rem;
    height: 2.25rem;
  }

  /* The deck sentence is longer than a number, so on a phone's two-column health
     row it takes the whole width rather than squeezing into half of it. */
  .match-standings .standing-opponent-deck {
    grid-column: 1 / -1;
  }

  .match-standings .standing-label {
    font-size: 0.72rem;
  }

  .match-standings .standing-value {
    font-size: 1.05rem;
  }

  .match-side,
  .match-log-wrap {
    padding: 0.5rem 0.75rem 0.65rem;
  }

  /* A collection card that has a picture turns on its side: the picture beside
     the name and the numbers rather than above them, so the whole set is a few
     screens of scrolling instead of a dozen, with the picture still big enough
     to know the card by. A card with no drawing keeps the stacked layout it
     falls back to. */
  .owned-card:has(.card-art),
  .unowned-card:has(.card-art) {
    grid-template-columns: minmax(0, 6rem) minmax(0, 1fr);
    align-items: center;
    column-gap: 0.75rem;
  }

  /* The picture keeps the first column, and every other part of the face —
     the head, the numbers, the ability text when there is one and the deck
     controls — is pinned to the second column explicitly. Auto-placement would
     otherwise drop the fourth part of a card that carries an ability into the
     narrow picture column once the picture stops at three rows. */
  .owned-card .card-art,
  .unowned-card .card-art {
    grid-column: 1;
    grid-row: 1 / span 4;
  }

  .owned-card:has(.card-art) > *:not(.card-art),
  .unowned-card:has(.card-art) > *:not(.card-art) {
    grid-column: 2;
    min-width: 0;
  }

  /* The three numbers share one line while there is room, and wrap onto two
     rather than run past the right edge on the narrowest phones. */
  .card-face-stats {
    flex-wrap: wrap;
    row-gap: 0.15rem;
  }

  /* A phone shows the same faces as a wide window, two to a row, so the hand
     and both tables are read by the same pictures at the size the creature is
     recognised by — the card never shrinks back into a chip. */
  .match-hand,
  .match-table {
    gap: 0.4rem;
  }

  /* A phone's face saves the line a caption would spend stacking the name over
     the numbers: they share one line under the picture, so a two-row hand still
     clears the action bar docked at the foot of the window. */
  .match-card-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.05rem 0.5rem;
  }

  .match-card {
    gap: 0.3rem;
    padding: 0.4rem 0.45rem 0.55rem;
  }

  /* A card control stays quieter than the rail's own actions — and the smaller
     font keeps a long "Play … (cost …)" label under its face. */
  .match-card-playable .match-play,
  .match-card-action .match-attack-select,
  .match-card-action .match-attack-target,
  .match-attack-hero {
    padding: 0.35rem 0.5rem;
    font-size: 0.78rem;
  }

  /* A refusal rides in the docked bar, which is fixed — so it is bounded to a
     couple of lines and scrolls, rather than growing tall enough to cover the
     hand it is warning about. */
  #screen-play .match-message:not(:empty) {
    max-height: 3.4rem;
    overflow-y: auto;
  }
}

/* Below the width where a picture column and a card's own parts can share a
   row, a card falls back to one column — picture, then name, numbers, ability
   and deck controls beneath it — so nothing is squeezed past the right edge.
   The two-column phone layout above this keeps the cards the size they read at
   on a phone. */
@media (max-width: 340px) {
  .owned-card:has(.card-art),
  .unowned-card:has(.card-art) {
    grid-template-columns: minmax(0, 1fr);
  }

  .owned-card .card-art,
  .unowned-card .card-art {
    grid-column: 1;
    grid-row: auto;
  }

  .owned-card:has(.card-art) > *:not(.card-art),
  .unowned-card:has(.card-art) > *:not(.card-art) {
    grid-column: 1;
  }

  /* Add and Remove take a row each here, so a long card name beside them does
     not push the pair past the card's edge. */
  .deck-actions {
    grid-template-columns: 1fr;
  }
}

/* --------------------------------------------------------------------------
   Text only assistive technology reads
   -------------------------------------------------------------------------- */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

@media (prefers-reduced-motion: reduce) {
  .screen,
  #screen-play,
  .match-result {
    animation: none;
  }

  /* A blow, and an ability landing, are told by the health numbers and the log
     when motion is unwanted: nothing lifts, shakes or fades, and the leaving
     card is not drawn at all. The ability's foil ring is not motion, so it
     stays and the card that acted is still marked. */
  .match-card.is-striking,
  .match-card.is-hit,
  .match-card.is-ability,
  .match-card-falling {
    animation: none;
  }

  .match-card-falling {
    display: none;
  }

  .btn {
    transition: none;
  }

  .btn:active {
    transform: none;
  }

  /* The deck opens at once rather than being watched open, and the caret shows
     which way it went without turning. */
  .opponent-deck-toggle,
  .opponent-deck-toggle::after {
    transition: none;
  }
}
