/* Sanctum — our own look. Deliberately nothing like Arena or MTGO. See LEGAL.md. */

:root {
  --bg: #0d1116;
  --panel: #151b23;
  --panel-2: #1c242e;
  --line: #29323f;
  --line-bright: #3a4657;
  --text: #d7dee7;
  --text-dim: #8b97a6;
  --accent: #c8a544;
  --accent-dim: #6b5a24;
  --danger: #c05555;
  /* A fifth bigger than they used to be — cards on a table you are reading from a
     few feet away, not thumbnails. Everything else is derived: the neat-mode rows, the
     hand overlap and the drag ghost all measure these rather than repeating them. */
  --card-w: 110px;
  --card-radius: 5px;
  --hand-card-w: 142px;

  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
  margin: 0;
}

body {
  display: grid;
  /* One column, pinned to the window and allowed to shrink. Without the explicit
     column the grid sizes itself to its widest content, so a big hand stretched the
     whole page sideways — and the hand then measured that stretched width, decided it
     had plenty of room, and never overlapped its cards to fit. */
  grid-template-columns: minmax(0, 1fr);
  /* Four rows, and every child is placed in one by name below rather than by the
     order it happens to appear in. Auto-placement broke the moment a fourth child was
     added between the header and the table: the new bar took the 1fr row meant for the
     board, the board collapsed to its content, and the table went black. Naming the row
     also means hiding a child cannot shift the others. */
  grid-template-rows: auto auto minmax(0, 1fr) auto;
  background: var(--bg);
  color: var(--text);
  font: 13px/1.45 ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
  overflow: hidden;
}

/* --- Top bar ------------------------------------------------------------ */

.topbar {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 8px 14px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
}

.brand {
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
  font-size: 12px;
}

.toolgroup {
  display: flex;
  gap: 6px;
}
.toolgroup-right {
  margin-left: auto;
  align-items: center;
}

/* Sets the two recovery buttons apart from the ordinary ones beside them. */
.toolgroup-sep {
  width: 1px;
  align-self: stretch;
  margin: 2px 4px;
  background: var(--line-bright);
}

.btn {
  padding: 6px 11px;
  border: 1px solid var(--line-bright);
  border-radius: 5px;
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  white-space: nowrap;
}
.btn:hover {
  background: #24303d;
  border-color: #4a5a6f;
}
.btn:active {
  transform: translateY(1px);
}
.btn-primary {
  background: var(--accent-dim);
  border-color: var(--accent);
  color: #f3e6c0;
}
.btn-primary:hover {
  background: #85701f;
}
/* Loud on purpose. Getting back into your own deck is the thing you reach for when
   something has gone wrong, so it should never need hunting for. */
.btn-alert {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 22%, transparent);
  color: #ffd9d9;
}
.btn-alert:hover {
  background: color-mix(in srgb, var(--danger) 40%, transparent);
  border-color: #e07070;
}

/* A toggle has to look pressed, or you cannot tell the mode from the invitation. */
.btn.is-on {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  color: #f3e6c0;
}

/* Lit up when the turn is actually yours, so the button says whose move it is too. */
.btn-pass.is-my-turn {
  background: var(--accent-dim);
  border-color: var(--accent);
  color: #f3e6c0;
  font-weight: 600;
}
.btn-pass.is-my-turn:hover {
  background: #85701f;
}

.btn-ghost {
  background: transparent;
  border-color: var(--line);
  color: var(--text-dim);
}
.btn-ghost:hover {
  color: var(--text);
}

/* --- Table layout ------------------------------------------------------- */

.table {
  display: grid;
  /* The left column is sized by the rail itself — see .rail-left, whose width is a
     question about height. The battlefield takes everything left over. */
  grid-template-columns: auto minmax(0, 1fr) 268px;
  gap: 10px;
  padding: 10px;
  min-height: 0;

  /* Makes this element's own height available to its descendants as cqh. Safe to
     contain: the height comes from the page grid, never from what is inside. */
  container-type: size;

  /**
   * Everything in the left rail that is not a card: four labels, three gaps, and the
   * commander-tax rows under the command zone. Generous by a row, so a partner deck
   * with two commanders still fits.
   */
  --rail-chrome: 150px;
}

.rail {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-height: 0;
}

/**
 * How wide the left rail is, is really a question about how tall it is.
 *
 * The four piles are card-shaped, so each one's height comes from the rail's width.
 * At a fixed 130px they need something like 850px of height between them whatever the
 * window is doing, and on anything shorter the last of them — the command zone — used
 * to hang out below the rail underneath the hand, where nothing in it could be clicked.
 *
 * So the width follows from the height instead: a quarter of the rail each, less the
 * labels and gaps, converted back through the card's aspect ratio. On a tall screen the
 * 130px cap wins and nothing changes; on a short one everything shrinks together and
 * stays where it belongs. The floor stops it shrinking to a sliver, and the scroll is
 * the backstop for a window too short even for that.
 */
.rail-left {
  width: clamp(40px, calc((100cqh - var(--rail-chrome)) / 4 * 488 / 680), 130px);

  /* Deliberately not clipped, and the floor above is low for the same reason: a card
     hovered on a pile grows to two and a half times its size and has to be able to
     reach out over the board to be read, exactly as one on the battlefield does. The
     width formula is what keeps the rail's own contents inside it — clipping was the
     wrong tool and it cut the top card of the library in half. */
  position: relative;
  z-index: 1; /* above the board, which is positioned; battlefield cards ride higher */

  /* So the tax rows below can drop parts of themselves as the rail narrows. */
  container-type: inline-size;
}

/**
 * A narrow rail sheds the label before it sheds the controls.
 *
 * The commander-tax row has to keep its number and both buttons at every width — being
 * unable to correct a count is the whole problem this is here to avoid. The name and
 * the cost are the parts you can look up in the tooltip instead.
 */
@container (max-width: 104px) {
  .command-tax .chip-label {
    display: none;
  }
}
@container (max-width: 78px) {
  .command-tax .chip-note {
    display: none;
  }
}

.rail-right {
  overflow: hidden;
}

/* --- Piles -------------------------------------------------------------- */

.pile {
  position: relative;
  cursor: pointer;
}

.pile-slot {
  position: relative;
  width: 100%;
  aspect-ratio: 488 / 680;
  border: 1px dashed var(--line-bright);
  border-radius: var(--card-radius);
  background: rgba(255, 255, 255, 0.015);
}

.pile.drop-hover .pile-slot,
.battlefield.drop-hover,
.hand-area.drop-hover {
  border-color: var(--accent);
  background: rgba(200, 165, 68, 0.08);
}

.pile-label {
  margin-top: 4px;
  font-size: 11px;
  color: var(--text-dim);
  text-align: center;
}

.count {
  display: inline-block;
  min-width: 18px;
  padding: 0 4px;
  border-radius: 9px;
  background: var(--panel-2);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* --- Battlefield -------------------------------------------------------- */

.battlefield {
  position: relative;
  border: 1px solid var(--line);
  border-radius: 8px;
  background:
    radial-gradient(120% 90% at 50% 0%, #17262a 0%, #101820 55%, #0c1015 100%);
  /* Not hidden: a card hovered near the edge doubles in size and has to be allowed to
     spill over the rails to stay readable. The play areas below round their own outer
     corners so the board still looks like a board. */
  overflow: visible;
}

/* --- Play areas --------------------------------------------------------- */
/* One quadrant per seat. These are backdrops only — pointer events pass through
   to the battlefield, which works out which quadrant a drop landed in. */

/* Size and position come from the layout, inline — a play area is a quarter, a half,
   or the whole table depending on how many people are sitting down. */
.quadrant {
  position: absolute;
  z-index: 0;
  pointer-events: none;
  border: 1px solid color-mix(in srgb, var(--seat-color) 30%, transparent);
  transition: opacity 0.2s, border-color 0.2s, left 0.25s, top 0.25s, width 0.25s, height 0.25s;

  /* Colour is strongest at the player's own outer edge and fades toward the middle of
     the table, so the tints never muddy the centre or fight with card art. */
  background: radial-gradient(
    130% 130% at var(--gx, 50%) var(--gy, 100%),
    color-mix(in srgb, var(--seat-color) 26%, transparent) 0%,
    color-mix(in srgb, var(--seat-color) 9%, transparent) 45%,
    transparent 78%
  );
}

.quadrant.is-you {
  border-color: color-mix(in srgb, var(--seat-color) 55%, transparent);
}

/* Whose turn it is. The glow is an inset shadow rather than a thicker border so the
   areas do not shift by a pixel every time the turn passes. */
.quadrant.is-active {
  border-color: var(--seat-color);
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--seat-color) 70%, transparent),
    inset 0 0 40px -10px color-mix(in srgb, var(--seat-color) 55%, transparent),
    0 0 18px -4px color-mix(in srgb, var(--seat-color) 45%, transparent);
}
.quadrant.is-active.is-empty {
  opacity: 0.6;
}

.quadrant.is-active .seat-plate {
  /* Tinted for whose turn it is, but no more see-through than any other plate — it has
     the board and neat mode's outlines behind it either way. */
  background: color-mix(in srgb, var(--seat-color) 20%, rgba(6, 9, 12, 0.97));
  border-color: var(--seat-color);
}

/* An unoccupied seat keeps its colour — you should be able to see all four areas
   before anyone sits down — but reads as clearly vacant. */
.quadrant.is-empty {
  opacity: 0.6;
  border-style: dashed;
}

/* --- Neat mode ---------------------------------------------------------- */
/* Three boxes inside a play area: the bulk of the board, a narrow column beside it,
   and the land row along the player's own edge. Hidden until that player asks for
   them, and never in the way — outlines only, and transparent to the pointer so a
   card can still be dropped straight through onto the table. */

.tidy-box {
  position: absolute;
  display: none;
  pointer-events: none;
  border: 1px dashed color-mix(in srgb, var(--seat-color) 26%, transparent);
  border-radius: 6px;
  box-sizing: border-box;
}
.quadrant.is-tidy .tidy-box {
  display: block;
}

/* Named quietly. You need it while you are learning where things go and never again,
   so it sits at the box's own top-left and stays out of the way of the cards. */
.tidy-label {
  position: absolute;
  top: 3px;
  left: 7px;
  color: color-mix(in srgb, var(--seat-color) 55%, transparent);
  font: 9px/1 ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  max-width: calc(100% - 14px);
}

/* Cards glide to their place rather than teleporting, so it is obvious that the board
   rearranged itself and which card moved where. */
.battlefield .card.tidied {
  transition: box-shadow 0.12s, outline-color 0.12s, left 0.18s ease, top 0.18s ease;
}


/* Somebody else's zones, on the corner of their own board.
   Small on purpose: this is for reading across the table at a glance — how much library
   is left, what is in the graveyard, whether the commander is home — not for working in.
   Hover any of them and the full card comes up in the preview panel like anything else. */
.seat-piles {
  display: flex;
  gap: 7px;
  padding-top: 8px;
  border-top: 1px solid color-mix(in srgb, var(--seat-color) 22%, transparent);
}

.seat-pile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
}

.seat-pile-slot {
  position: relative;
  display: block;
  width: 46px;
  aspect-ratio: 488 / 680;
  border-radius: 4px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.7);
}

/**
 * An empty zone, drawn rather than left out.
 *
 * The same outline the rail uses for a pile with nothing in it, so a zero here reads as
 * "I have looked, and there is nothing in there" rather than as a gap in the row. It
 * keeps the four chips in the same places under every seat, which is what makes the
 * whole row scannable across the table.
 */
.seat-pile.is-empty .seat-pile-slot {
  border: 1px dashed var(--line-bright);
  background: rgba(255, 255, 255, 0.015);
  box-shadow: none;
}
.seat-pile.is-empty .seat-pile-count {
  color: var(--muted);
}

/* At this size the ring on the default back is bigger than anything else in the chip
   and reads as a symbol rather than as the back of a card. The pattern alone is enough. */
.seat-pile-slot .card-back::after {
  content: none;
}

/* The card fills its slot instead of being placed on a board, as in the rail.
   Written with the plate in the selector, and it has to be. These chips live inside
   #battlefield — the plate sits in a quadrant — so ".battlefield .card" also matches
   them, and it carries the translate(-50%, -50%) that positions a card by its middle on
   the board. Both selectors weigh the same, so that one wins on being written later and
   the thumbnails sit half their own size up and to the left of the slots they belong to.
   The rail is unaffected only because it is outside the battlefield. */
.seat-plate .seat-pile-slot .card {
  position: absolute;
  inset: 0;
  width: 100%;
  transform: none;
  box-shadow: none;
  cursor: pointer;
}
.seat-plate .seat-pile-slot .card.tapped {
  transform: none;
}

.seat-pile-count {
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  color: color-mix(in srgb, var(--seat-color) 70%, #fff);
  font-variant-numeric: tabular-nums;
}

/* Short enough to sit under a 34px card. "Graveyard" does not fit and "GY" is jargon;
   these are the words people actually say at a table. */
.seat-pile-label {
  font-size: 9px;
  line-height: 1.1;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

.seat-pile:hover .seat-pile-slot {
  box-shadow: 0 0 0 1px var(--seat-color), 0 2px 6px rgba(0, 0, 0, 0.8);
}

/* A chip that is only a count. Your own deck does not open from here — see the note in
   renderSeatPiles — so it must not offer to, either by the cursor or by lighting up. */
.seat-pile.is-static {
  cursor: default;
}
.seat-pile.is-static:hover .seat-pile-slot {
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.7);
}

/* Big enough to actually look at. Somebody else's graveyard top or the picture on
   their sleeves is worth a proper glance, and these are a third of a card otherwise. */
.seat-plate .seat-pile-slot .card {
  transition: transform 0.12s;
  transform-origin: center;
}
.seat-plate .seat-pile:hover .seat-pile-slot .card {
  transform: scale(3.2);
  z-index: 400;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.8);
}
.seat-plate {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 7px;
  padding: 8px 11px 9px;
  border: 1px solid color-mix(in srgb, var(--seat-color) 45%, transparent);
  border-radius: 8px;
  background: rgba(6, 9, 12, 0.96);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.55);
  color: var(--seat-color);
  /* Dragging across the board used to select the plate's text, which then sat there
     highlighted looking like something was chosen. */
  user-select: none;
  font: 13px/1.4 ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
  pointer-events: auto; /* the quadrant itself stays transparent to card drops */
  transition: border-color 0.15s, background 0.15s;
}
.seat-plate:hover {
  background: rgba(6, 9, 12, 0.99);
  border-color: color-mix(in srgb, var(--seat-color) 75%, transparent);
}

/* The identity line: swatch, name, life, board size. Opens the seat menu. */
.seat-id {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-areas:
    "swatch name  vitals"
    "meta   meta  vitals";
  align-items: center;
  column-gap: 9px;
  row-gap: 3px;
  text-align: left;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  cursor: pointer;
  white-space: nowrap;
}

/* Commander damage, sitting directly under the life total where you look for it. */
.seat-damage {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
}
.seat-damage:empty {
  display: none;
}

/* A number with a way up and a way down. Used on the seat plates for commander
   damage and in the rail for commander tax; both are counts kept by hand, so both
   need both directions in plain sight rather than behind a modifier key. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 0 2px;
  border: 1px solid color-mix(in srgb, var(--seat-color) 45%, transparent);
  border-radius: 3px;
  background: color-mix(in srgb, var(--seat-color) 12%, rgba(8, 11, 15, 0.6));
  color: var(--seat-color);
  font: 10px/1.6 inherit;
  max-width: 100%;
}

.chip-step {
  flex: none;
  width: 13px;
  padding: 0;
  border: 0;
  border-radius: 2px;
  background: color-mix(in srgb, var(--seat-color) 18%, transparent);
  color: inherit;
  font: inherit;
  line-height: 1.4;
  text-align: center;
  cursor: pointer;
}
.chip-step:hover {
  background: color-mix(in srgb, var(--seat-color) 55%, transparent);
  color: #f3e6c0;
}

.chip-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0.85;
}
.chip-value {
  flex: none;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* What the count costs you — secondary to the count itself. */
.chip-note {
  flex: none;
  color: var(--text-dim);
  font-size: 9px;
  font-variant-numeric: tabular-nums;
}

/* 21 from a single commander is lethal, so it stops being subtle. */
.chip.lethal {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 35%, transparent);
  color: #ffd9d9;
}

.seat-swatch {
  grid-area: swatch;
  width: 9px;
  height: 9px;
  border-radius: 2px;
  background: var(--seat-color);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
  flex: 0 0 auto;
}
.q-top .seat-plate {
  top: 8px;
}
.q-bottom .seat-plate {
  bottom: 8px;
}
.q-left .seat-plate {
  left: 8px;
}
.q-right .seat-plate {
  right: 8px;
}

/* How many cards they are holding, drawn as a little card so it cannot be mistaken
   for the life total sitting next to it. */
.seat-hand {
  padding: 1px 6px;
  border: 1px solid color-mix(in srgb, var(--seat-color) 45%, transparent);
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.04);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.seat-hand[hidden] {
  display: none;
}
/* Said in the markup rather than here would put it in the tooltip too, where it already
   says the same thing at more length. This is the glance-sized version. */
.seat-hand::after {
  content: " in hand";
  color: var(--muted);
}

.seat-name {
  grid-area: name;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The number everyone actually looks for, given the size to say so. A name and four
   numbers all set the same way read as one long number, which is what this used to do. */
.seat-vitals {
  grid-area: vitals;
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1;
  padding-left: 4px;
}
.seat-life-cap {
  font-size: 9px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Cards in hand and permanents out: read often, but never before the life total, so
   they get a line of their own under the name rather than a place in it. */
.seat-meta {
  grid-area: meta;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  white-space: nowrap;
}
.seat-life {
  font-size: 28px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.seat-perms {
  color: var(--text-dim);
}

.quadrant.is-empty .seat-plate {
  opacity: 0.5;
}

/* Permanents carry a hairline in their seat's colour so ownership reads at a glance
   once four people have things on the table. */
.battlefield .card {
  /* Set per card when a tidied row fans and overlaps, so the cards stack left to right.
     A property rather than z-index directly: the rules below have to be able to lift a
     hovered or dragged card over it, and an inline z-index would outrank all of them. */
  z-index: var(--stack-z, 1);

  /* How big this card was left sitting on the table — see the wheel while dragging.
     Every transform below multiplies through it rather than replacing it. */
  transform: translate(-50%, -50%) scale(var(--card-scale, 1));
  box-shadow:
    0 2px 6px rgba(0, 0, 0, 0.55),
    0 0 0 1.5px color-mix(in srgb, var(--seat-color, #c8a544) 55%, transparent);
}

.empty-hint {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  color: var(--text-dim);
  text-align: center;
  padding: 20px;
}
.empty-hint h2 {
  margin: 0;
  font-size: 16px;
  font-weight: 500;
  color: var(--text);
}
.empty-hint p {
  margin: 0 0 10px;
  max-width: 44ch;
}
.empty-hint[hidden] {
  display: none;
}

/* --- Cards -------------------------------------------------------------- */

.card {
  position: absolute;
  width: var(--card-w);
  aspect-ratio: 488 / 680;
  border-radius: var(--card-radius);
  transform: translate(-50%, -50%);
  transform-origin: center;
  cursor: grab;
  user-select: none;
  touch-action: none;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.55);
  transition: box-shadow 0.12s, outline-color 0.12s;
  outline: 2px solid transparent;
  background: var(--panel-2);
}

.card img {
  width: 100%;
  height: 100%;
  border-radius: inherit;
  display: block;
  pointer-events: none;
}

/**
 * A card on its way off the top of the deck.
 *
 * A copy, flown over the table on `position: fixed` so it is measured against the window
 * and crosses the rail, the board and the hand without any of them clipping it. It is
 * scenery — nothing can click it, and the real card is already in the hand underneath.
 *
 * Both ends are set from JavaScript, which measures them; the only thing decided here is
 * how the journey feels. The easing arrives quickly and settles, the way a card pushed
 * across a table does. The duration is read back out of here by table.js rather than
 * written down in both places.
 */
:root {
  --draw-flight: 300ms;
}

.card.draw-flight {
  position: fixed;
  margin: 0;
  transform-origin: top left;
  transition: transform var(--draw-flight) cubic-bezier(0.22, 0.61, 0.36, 1);
  pointer-events: none;
  cursor: default;
  z-index: 3000;
  /* Lifted off the table for the trip, so it reads as travelling over the board rather
     than sliding along underneath it. */
  box-shadow: 0 12px 28px rgba(0, 0, 0, 0.62);
}

.card.tapped {
  transform: translate(-50%, -50%) rotate(90deg);
}
.battlefield .card.tapped {
  transform: translate(-50%, -50%) rotate(90deg) scale(var(--card-scale, 1));
}

.card:hover,
.card.peek {
  outline-color: var(--accent);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.7);
  z-index: 50;
}

/**
 * A card only ever owns the pointer over the space it really occupies.
 *
 * Hovering blows a card up to two and a half times its size, and that enlarged body used
 * to keep hold of the pointer — so sweeping along a row, the first card stayed up and
 * covered the next three, and you had to swing the mouse right around it to reach them.
 *
 * So the enlarged part stops taking pointer events, and this child scales back down by
 * exactly as much as the card scaled up, leaving a hit area the size and shape of the
 * card as it actually sits on the table. Move past that and the card underneath takes
 * over, which is what makes gliding across a row work.
 *
 * It carries the card's own transition so the two grow and shrink together: without
 * that the hit area would snap small while the card was still growing, and the card
 * would drop out from under the pointer that had just arrived on it.
 */
.card-hit {
  position: absolute;
  inset: 0;
  transition: transform 0.12s ease 0.12s;
}

.battlefield .card:hover,
.hand .card:hover,
.hand .card.peek {
  pointer-events: none;
}
.battlefield .card:hover .card-hit,
.hand .card:hover .card-hit,
.hand .card.peek .card-hit {
  pointer-events: auto;
}

.battlefield .card:hover .card-hit {
  transform: scale(calc(1 / var(--zoom)));
  transform-origin: var(--ox, center) var(--oy, center);
}
/* A tapped card is rotated about its centre, so its footprint is too. */
.battlefield .card.tapped:hover .card-hit {
  transform-origin: center;
}
/* The hand lifts as well as grows; the lift is undone after the scale so it cancels. */
.hand .card:hover .card-hit,
.hand .card.peek .card-hit {
  transform: scale(calc(1 / var(--zoom))) translateY(8px);
  transform-origin: bottom var(--ox, center);
}

/* Counters stay reachable — that is how the wheel gets at them — but nothing else
   inside a blown-up card takes the pointer. */
.battlefield .card:hover .counter-chip {
  pointer-events: auto;
}

/* --- Hover to read ------------------------------------------------------ */
/* Cards on the table are too small to read. Hovering blows one up in place so you
   can read it without moving your eyes off the board.
   The delay stops the whole table strobing as the cursor crosses it. */

/* How much a hovered card grows. Cards on the table are far too small to read, and
   this is the only way to read one without taking your eyes off the board. */
:root {
  --zoom: 2.6;

  /* The rail gets its own, smaller number. A pile top starts out a little larger than a
     card on the board and grows out of a corner rather than in place, so the same
     multiplier put a 300px card across the left of the table every time the cursor
     crossed the library on its way somewhere else. This lands near the size of a real
     card in the hand, which is the size at which one is meant to be read. */
  --zoom-pile: 2;
}

.card:hover {
  transition: transform 0.12s ease 0.12s, box-shadow 0.12s, outline-color 0.12s;
}
/* No delay on a peeked card: the delay is there to stop the table strobing as the
   cursor crosses it, and a wheel notch is a deliberate act with nothing to debounce. */
.card.peek {
  transition: transform 0.12s ease, box-shadow 0.12s, outline-color 0.12s;
}

.battlefield .card:hover {
  /* --ox/--oy are set per card from its position, so it grows into the table rather
     than off the edge of the screen. */
  transform-origin: var(--ox, center) var(--oy, center);
  transform: translate(-50%, -50%) scale(calc(var(--card-scale, 1) * var(--zoom)));
  z-index: 200;
}
/* A tapped card keeps a centred origin: it is rotated, and rotating about a corner
   would fling it across the board. It may clip slightly at the very edge; that is the
   better trade. */
.battlefield .card.tapped:hover {
  transform-origin: center;
  transform: translate(-50%, -50%) rotate(90deg) scale(calc(var(--card-scale, 1) * var(--zoom)));
}

/* A card being dragged is already following the cursor; blowing it up as well makes
   it impossible to place. */
.card.dragging,
.card.dragging:hover {
  transform: translate(-50%, -50%) !important;
  transition: none;
}

.card.dragging {
  cursor: grabbing;
  opacity: 0.85;
  z-index: 999;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.8);
}

/* Carried along by a group drag. They dim where they stand like the card under the
   cursor, so it reads as the whole highlight being in flight rather than the one you
   happened to grab — but they keep the size they were left at, because nothing about
   them has changed yet. */
.card.dragging-along {
  opacity: 0.4;
  transition: none;
}

/* The copy that follows the cursor while a card is being dragged. The real card stays
   where it is and dims; this is what shows where it will land, so it is placed straight
   from the pointer with no transform of its own. */
.drag-ghost {
  opacity: 0.85;
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.8);
  transition: none;
}

/* Our own card back — no borrowed artwork. */
.card-back {
  width: 100%;
  height: 100%;
  border-radius: inherit;
  background:
    repeating-linear-gradient(45deg, #1b2530 0 6px, #16202a 6px 12px),
    linear-gradient(160deg, #24313f, #121a22);
  border: 1px solid #33414f;
  display: grid;
  place-items: center;
}
.card-back::after {
  content: "";
  width: 42%;
  aspect-ratio: 1;
  border: 2px solid var(--accent-dim);
  border-radius: 50%;
  box-shadow: inset 0 0 0 3px rgba(200, 165, 68, 0.12);
}

/* A player's own picture on the back of their cards. The default pattern and its ring
   are dropped entirely — the whole point is that this looks like somebody's sleeves and
   not like the table's card back with a decal on it. */
.card-back.sleeved {
  background-color: #12181f;
  border-color: color-mix(in srgb, var(--seat-color, #c8a544) 45%, #33414f);
}

/* Fills the card and crops the overflow, rather than squashing somebody's picture. */
.card .sleeve-img {
  object-fit: cover;
}
.card-back.sleeved::after {
  content: none;
}

/* A permanent whose controller is not its owner. The hairline is the controller's
   colour; this pip is the owner's, so you can see where it goes when it dies. */
.card.borrowed::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--owner-color);
  box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.75);
  z-index: 2;
  pointer-events: none;
}

/**
 * Counters sit across the middle of the art.
 *
 * The art is the one part of a card you never have to read. Everything else is
 * information you might: the name at the top, the type line and rules text through the
 * middle, and the power and toughness in the corner — which is where these used to sit,
 * covering the number most worth knowing on a creature that has +1/+1 counters on it.
 *
 * 27% down is the middle of the art window on a modern frame, below the name bar and
 * well above the type line, so even three rows of counters stay clear of the text.
 */
.card-counters {
  position: absolute;
  left: 2px;
  right: 2px;
  top: 27%;
  transform: translateY(-50%);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2px;
  pointer-events: none;
}

.counter-chip {
  /* The wrap stays transparent so the gaps between chips do not swallow a drag, but
     each chip itself is reachable — hovering one and rolling the wheel changes it. */
  pointer-events: auto;
  cursor: ns-resize;
  padding: 0 4px;
  border-radius: 3px;
  /* Nearly opaque, and lifted with a shadow: these now sit over artwork rather than a
     dark frame, and art is the least predictable background there is. */
  background: rgba(8, 11, 15, 0.93);
  border: 1px solid var(--accent-dim);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.75);
  color: var(--accent);
  font-size: 10px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.5;
}

/* A commander, wherever it is. Deliberately not a seat colour — a seat can be amber and
   this has to stay distinguishable from "whose card is this". */
:root {
  --commander: #e8c860;
}

.card.is-commander {
  box-shadow:
    0 0 0 2px var(--commander),
    0 0 14px -2px color-mix(in srgb, var(--commander) 70%, transparent),
    0 2px 6px rgba(0, 0, 0, 0.55);
}
.battlefield .card.is-commander {
  /* Keep the owner hairline underneath, so it still reads as theirs. */
  box-shadow:
    0 0 0 2px var(--commander),
    0 0 0 3.5px color-mix(in srgb, var(--seat-color, #c8a544) 70%, transparent),
    0 0 16px -2px color-mix(in srgb, var(--commander) 70%, transparent),
    0 2px 6px rgba(0, 0, 0, 0.55);
}

.token-badge {
  position: absolute;
  top: 2px;
  right: 2px;
  padding: 0 4px;
  border-radius: 3px;
  background: rgba(10, 14, 18, 0.9);
  color: var(--text-dim);
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  pointer-events: none;
}

/* --- Commander tax ------------------------------------------------------ */
/* Under the command zone, because that is where you look when you are working out
   whether you can afford to cast it again. */

.command-tax {
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin-top: 3px;
}

/* --- Picking out several cards ------------------------------------------ */
/* Hold the right button and sweep a box over the board, then click any one of the
   cards caught in it to tap or untap all of them together. */

.marquee {
  position: absolute;
  z-index: 150;
  pointer-events: none;
  border: 1px solid var(--accent);
  border-radius: 3px;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.marquee[hidden] {
  display: none;
}

/* Bright enough to count at a glance while the box is still being dragged — the point
   is knowing what you are about to tap before you let go. */
.battlefield .card.selected,
.hand .card.selected {
  outline: 2px solid var(--accent);
  box-shadow:
    0 2px 6px rgba(0, 0, 0, 0.55),
    0 0 0 4px color-mix(in srgb, var(--accent) 35%, transparent);
}

/* Hand cards overlap, so an outline on one is half-covered by the next along. Lifting a
   picked card clear of the row is what makes a handful of them countable at a glance —
   and it is the same lift the hover uses, so it reads as the card standing forward. */
.hand .card.selected {
  transform: translateY(-10px);
  z-index: 150;
}
.hand .card.selected:hover {
  z-index: 200;
}

/* --- Stacked tokens ----------------------------------------------------- */
/* A hundred identical tokens are one pile of cards on a real table, so they are drawn
   as one card carrying a count. The stepped edges behind it say "there are more of
   these" before you have read the number. */

.battlefield .card.is-stack {
  box-shadow:
    0 2px 6px rgba(0, 0, 0, 0.55),
    0 0 0 1.5px color-mix(in srgb, var(--seat-color, #c8a544) 55%, transparent),
    /* the cards underneath */
    3px 3px 0 -1px var(--panel-2),
    3px 3px 0 0 color-mix(in srgb, var(--seat-color, #c8a544) 30%, transparent),
    6px 6px 0 -1px var(--panel-2),
    6px 6px 0 0 color-mix(in srgb, var(--seat-color, #c8a544) 20%, transparent);
}

/* Big enough to read across the table without picking the card up, which is the whole
   reason the pile is drawn as one card. */
.stack-badge {
  position: absolute;
  bottom: 3px;
  right: 3px;
  min-width: 20px;
  padding: 1px 5px;
  border: 1px solid color-mix(in srgb, var(--seat-color, #c8a544) 60%, transparent);
  border-radius: 9px;
  background: rgba(8, 11, 15, 0.92);
  color: #f3e6c0;
  font: 600 13px/1.3 ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
  text-align: center;
  pointer-events: none;
}

/* Pile top card sits inside the slot rather than free-floating. */
.pile-slot .card {
  position: absolute;
  inset: 0;
  width: 100%;
  transform: none;
}
.pile-slot .card.tapped {
  transform: none;
}

/* --- Hand --------------------------------------------------------------- */

.hand-area {
  position: relative; /* the sort caret is placed against this */
  border-top: 1px solid var(--line);
  background: var(--panel);
  padding: 6px 14px 10px;
  min-height: 128px;
}

.hand-label {
  font-size: 11px;
  color: var(--text-dim);
  margin-bottom: 4px;
}

.hand {
  display: flex;
  align-items: flex-start;
  min-height: 100px;
  min-width: 0; /* so a wide hand shrinks rather than pushing the page out */
}

/* Where a card being sorted will land. Hand cards overlap once there are more than a
   few of them, so without this you are aiming at a gap you cannot see. */
.hand-caret {
  position: absolute;
  top: 24px;
  bottom: 8px;
  width: 2px;
  border-radius: 1px;
  background: var(--accent);
  box-shadow: 0 0 6px var(--accent);
  pointer-events: none;
  z-index: 300;
}

.hand .card {
  position: relative;
  width: var(--hand-card-w);
  flex: 0 0 auto;
  transform: none;
  margin-right: 4px;
}
/* Grows upward out of the hand, since there is nothing below it — and inward from
   whichever side it is nearest, so the card at the end of the row does not grow off the
   edge of the window. --ox is measured per card in anchorHandZoom. */
.hand .card:hover,
.hand .card.peek {
  transform: translateY(-8px) scale(var(--zoom));
  transform-origin: bottom var(--ox, center);
  z-index: 200;
}

/* While the wheel is walking the row, the card the pointer happens to be resting on
   stands down. Two cards blown up at once and neither is the one being read. */
.hand.peeking .card:hover:not(.peek) {
  transform: none;
  z-index: auto;
  outline-color: transparent;
}
.hand .card.tapped {
  transform: none;
}

/**
 * Pile tops grow rightward, into the board rather than off the left edge.
 *
 * And away from whichever end of the rail they sit at. Growing from the middle sends
 * the top pile up past the toolbar and the bottom one down past the hand, both of which
 * are the edge of the window — so the top two grow downwards and the bottom two
 * upwards, and all four stay on screen.
 *
 * Which pile is which is a question about where it currently sits, not what it holds:
 * the rail can be reordered, and keying this to the zone meant a library moved to the
 * bottom grew down off the screen. applyRailOrder sets the half.
 */
.pile-slot .card:hover {
  transform: scale(var(--zoom-pile));
  z-index: 200;
}
.pile[data-rail-half='top'] .card:hover {
  transform-origin: left top;
}
.pile[data-rail-half='bottom'] .card:hover {
  transform-origin: left bottom;
}

/* Not inside the pile viewer: that grid scrolls, so a zoomed card would be clipped by
   its own container. The cards there are already large enough to read. */
.pile-grid .card:hover {
  transform: none;
}

/* --- Right rail --------------------------------------------------------- */

.preview {
  position: relative;
  width: 100%;
  aspect-ratio: 488 / 680;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  overflow: hidden;
  flex: 0 0 auto;
}
.preview img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: none;
}
.preview img.visible {
  display: block;
}
.preview-empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--text-dim);
  font-size: 12px;
}
.preview-empty[hidden] {
  display: none;
}

.life-widget {
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  padding: 8px;
  flex: 0 0 auto;
}

.life-row {
  display: flex;
  align-items: center;
  gap: 5px;
}
.life-step {
  padding: 4px 7px;
  font-variant-numeric: tabular-nums;
}
.life-total {
  flex: 1;
  text-align: center;
  font-size: 26px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* --- Trackers ----------------------------------------------------------- */
/* Anything Magic asks you to count: poison, energy, rad, commander damage, and any
   counter type that exists or is printed next year. */

.tracker-panel {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  padding: 8px;
  gap: 4px;
  flex: 0 0 auto;
}
.tracker-title {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

.tracker-list {
  display: flex;
  flex-direction: column;
  gap: 3px;
  max-height: 168px;
  overflow-y: auto;
}

.tracker-row {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 2px 4px;
  border-radius: 4px;
  background: rgba(255, 255, 255, 0.02);
}
.tracker-row.has-source {
  border-left: 2px solid var(--seat-color);
}
/* Lethal, or about to be: 10 poison, 21 from one commander. */
.tracker-row.danger {
  background: color-mix(in srgb, var(--danger) 20%, transparent);
}
.tracker-row.danger .tracker-value {
  color: var(--danger);
}

.tracker-label {
  flex: 1;
  min-width: 0;
  font-size: 11px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tracker-value {
  min-width: 20px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.tracker-step {
  padding: 0 6px;
  line-height: 18px;
  border-radius: 4px;
}
.tracker-empty {
  font-size: 11px;
  padding: 2px 4px;
}
.tracker-add {
  margin-top: 6px;
  width: 100%;
  font-size: 11px;
}

/* Monarch, initiative, city's blessing — exclusive, mobile, and easily forgotten. */
.designations {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 8px 0 2px;
}
.designation {
  padding: 2px 7px;
  border: 1px solid var(--line-bright);
  border-radius: 10px;
  background: transparent;
  color: var(--text-dim);
  font: 10px/1.6 inherit;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  cursor: pointer;
  white-space: nowrap;
}
.designation.held {
  border-color: var(--seat-color);
  color: var(--seat-color);
}
.designation.mine {
  background: color-mix(in srgb, var(--seat-color) 22%, transparent);
  font-weight: 600;
}

/* Said at the moment of decision. An empty command zone is otherwise discovered
   several turns into a game, which is much too late to be useful. */
.deck-warning {
  padding: 6px 9px;
  border: 1px solid var(--commander);
  border-radius: 5px;
  background: color-mix(in srgb, var(--commander) 15%, transparent);
  color: var(--commander);
  font-size: 12px;
}
.deck-warning[hidden] {
  display: none;
}

/* The pasted list, echoed back so you can point at your commander. */
.deck-preview {
  display: flex;
  flex-direction: column;
  gap: 1px;
  max-height: 190px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: #0f151c;
  padding: 4px;
}

.deck-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 2px 6px;
  border-radius: 3px;
  font-size: 12px;
  cursor: pointer;
  user-select: none;
}
.deck-row:hover {
  background: rgba(255, 255, 255, 0.05);
}
/* Same gold as the ring on the card itself, so the two read as the same thing. */
.deck-row.is-commander {
  background: color-mix(in srgb, var(--commander) 22%, transparent);
  color: var(--commander);
  font-weight: 600;
}
.deck-row.is-commander .deck-name::after {
  content: " — commander";
  font-weight: 400;
  opacity: 0.75;
}

.deck-count {
  min-width: 26px;
  text-align: right;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* --- Dice --------------------------------------------------------------- */

.dice-result {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  min-height: 96px;
  justify-content: center;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #0f151c;
}
.dice-result-empty {
  color: var(--text-dim);
  font-size: 12px;
}
.dice-who {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}
.dice-faces {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  justify-content: center;
}
.dice-face {
  min-width: 38px;
  padding: 4px 8px;
  border: 1px solid var(--line-bright);
  border-radius: 5px;
  background: var(--panel-2);
  text-align: center;
  font-size: 20px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* A natural max or a natural 1, readable without reading the number. */
.dice-face.high {
  border-color: var(--commander);
  color: var(--commander);
}
.dice-face.low {
  border-color: var(--danger);
  color: var(--danger);
}
.dice-total {
  font-size: 12px;
  color: var(--text-dim);
}

.dice-count {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text-dim);
}
#dice-count-value {
  min-width: 20px;
  text-align: center;
  font-weight: 600;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.dice-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.dice-die {
  min-width: 56px;
  padding: 8px 10px;
  font-size: 13px;
  font-weight: 600;
}
.dice-coin {
  margin-left: auto;
}

.counter-presets {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
}
.counter-preset {
  padding: 4px 9px;
  font-size: 12px;
}
.counter-custom {
  display: flex;
  gap: 6px;
}
.counter-custom input {
  flex: 1;
  padding: 6px 8px;
  border: 1px solid var(--line-bright);
  border-radius: 5px;
  background: #0f151c;
  color: var(--text);
  font: inherit;
}

/* --- Dice on the table --------------------------------------------------- */
/* A roll lands in the middle of the roller's own area so the whole table watches it
   happen. Big enough to read from any chair, and it clears itself afterwards. */


/* Something happening on somebody's board, said loudly.
   Sat above the middle of the area rather than on it, so a banner and a dice roll can
   both be on the same patch of table without covering each other. Deliberately large:
   the point is that somebody looking at their own hand still catches it. */
.announce {
  position: absolute;
  left: 50%;
  top: 34%;
  transform: translate(-50%, -50%);
  z-index: 300;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 10px 26px;
  border-radius: 12px;
  border: 1px solid color-mix(in srgb, var(--seat-color) 70%, transparent);
  background: rgba(8, 11, 15, 0.9);
  box-shadow:
    0 8px 34px rgba(0, 0, 0, 0.8),
    0 0 0 6px color-mix(in srgb, var(--seat-color) 12%, transparent);
  animation: announce-in 0.28s cubic-bezier(0.2, 1.5, 0.4, 1);
  transition: opacity 0.7s ease;
  white-space: nowrap;
}

.announce.fading {
  opacity: 0;
}

.announce-who {
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--seat-color) 85%, #fff);
}

.announce-what {
  /* Scales with the window rather than being pinned, because the whole point is that it
     is legible from the other side of the room on a television. */
  font-size: clamp(22px, 2.1vw, 40px);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: 0.01em;
  color: #f2ede1;
}

@keyframes announce-in {
  from {
    transform: translate(-50%, -50%) scale(0.7);
    opacity: 0;
  }
  to {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
  }
}
.roll-result {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 250;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 10px 14px;
  border: 1px solid var(--seat-color);
  border-radius: 10px;
  background: rgba(8, 11, 15, 0.88);
  box-shadow: 0 6px 26px rgba(0, 0, 0, 0.7);
  animation: roll-land 0.35s cubic-bezier(0.2, 1.5, 0.4, 1);
  transition: opacity 1.2s ease;
}
.roll-result.fading {
  opacity: 0;
}

/* A small bounce as it lands, so it catches the eye of somebody looking elsewhere. */
@keyframes roll-land {
  from {
    transform: translate(-50%, -50%) scale(0.6);
    opacity: 0;
  }
  to {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
  }
}

.roll-faces {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 5px;
  max-width: 260px;
}

.roll-face {
  min-width: 34px;
  padding: 2px 6px;
  border: 1px solid color-mix(in srgb, var(--seat-color) 60%, transparent);
  border-radius: 6px;
  background: color-mix(in srgb, var(--seat-color) 16%, transparent);
  color: #f3e6c0;
  font: 700 24px/1.25 ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.roll-caption {
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--seat-color);
}

/* Finding one card in a hundred. Only appears once a pile is big enough to hunt in. */
#pile-search {
  width: 100%;
  margin-bottom: 8px;
  padding: 7px 10px;
  border: 1px solid var(--line-bright);
  border-radius: 5px;
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
}
#pile-search:focus {
  outline: none;
  border-color: var(--accent);
}
#pile-search[hidden] {
  display: none;
}

/* --- Commander damage --------------------------------------------------- */
/* Every commander that can kill you, with what it has already got through for. The
   picture is the point: a shortened name and a number tells you that something called
   Atraxa has hit you for seven, and nothing about what Atraxa does. */

.commanders {
  flex: 0 0 auto;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  overflow: hidden;
}

.panel-title {
  padding: 6px 10px;
  border-bottom: 1px solid var(--line);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

.commander-list {
  display: flex;
  flex-direction: column;
}
.commander-list .hint {
  margin: 0;
  padding: 8px 10px;
}

.commander-row {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 8px;
  border-bottom: 1px solid var(--line);
  border-left: 3px solid var(--seat-color);
  cursor: ns-resize; /* the wheel works here too */
}
.commander-row:last-child {
  border-bottom: 0;
}
.commander-row:hover {
  background: rgba(255, 255, 255, 0.03);
}

/* The top of the card — art and name, which is all that fits and all you need to know
   it by. Hovering the row puts the whole card in the preview above. */
.commander-art {
  flex: none;
  width: 34px;
  height: 26px;
  object-fit: cover;
  object-position: top center;
  border-radius: 3px;
  background: var(--panel-2);
}

.commander-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 11px;
}

.commander-step {
  flex: none;
  width: 18px;
  padding: 0;
  border: 1px solid var(--line-bright);
  border-radius: 3px;
  background: var(--panel-2);
  color: var(--text-dim);
  font: inherit;
  font-size: 12px;
  line-height: 1.3;
  cursor: pointer;
}
.commander-step:hover {
  color: var(--text);
  border-color: #4a5a6f;
}

.commander-value {
  flex: none;
  min-width: 18px;
  text-align: center;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Twenty-one from a single commander ends the game, so it stops being subtle. */
.commander-row.lethal {
  background: color-mix(in srgb, var(--danger) 28%, transparent);
  color: #ffd9d9;
}

.log-panel {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  min-height: 0;
  flex: 1 1 auto;
  overflow: hidden;
}
.log-title {
  padding: 6px 10px;
  border-bottom: 1px solid var(--line);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}
.log {
  margin: 0;
  padding: 6px 10px;
  list-style: none;
  overflow-y: auto;
  font-size: 11px;
  color: var(--text-dim);
  min-height: 0;
}
.log li {
  padding: 1px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}
/* Client-side notices — lookup failures and hints. Not part of the shared table log. */
.log li.local {
  color: var(--text-dim);
  font-style: italic;
}

.connection {
  align-self: center;
  padding: 3px 9px;
  border: 1px solid var(--line-bright);
  border-radius: 5px;
  font-size: 11px;
  color: var(--text-dim);
}
.connection.bad {
  border-color: var(--danger);
  color: var(--danger);
}
/* Quiet by design: a game that is being kept safely should be reassuring to glance at,
   not something demanding attention. It only speaks up when it goes stale. */
.saved {
  align-self: center;
  font-size: 11px;
  color: var(--text-dim);
  opacity: 0.65;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.saved.stale {
  color: var(--danger);
  opacity: 1;
}
.saved[hidden] {
  display: none;
}

.connection[hidden] {
  display: none;
}

/* --- Dialogs ------------------------------------------------------------ */

.dialog {
  border: 1px solid var(--line-bright);
  border-radius: 10px;
  background: var(--panel);
  color: var(--text);
  padding: 0;
  width: min(560px, 92vw);
}
.dialog-wide {
  width: min(1000px, 94vw);
}
.dialog::backdrop {
  background: rgba(0, 0, 0, 0.6);
}

.dialog-body {
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.dialog-body h2 {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
}
.hint {
  margin: 0;
  font-size: 12px;
  color: var(--text-dim);
}
.hint code {
  background: var(--panel-2);
  padding: 1px 4px;
  border-radius: 3px;
}

.dialog textarea,
.dialog input[type="search"] {
  width: 100%;
  padding: 8px;
  border: 1px solid var(--line-bright);
  border-radius: 5px;
  background: #0f151c;
  color: var(--text);
  font: 12px/1.5 ui-monospace, "Cascadia Code", Consolas, monospace;
  resize: vertical;
}

.status {
  font-size: 12px;
  color: var(--text-dim);
}
.status.error {
  color: var(--danger);
}
.status[hidden] {
  display: none;
}

.dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin: 4px 0 0;
  padding: 0;
}

.pile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
  gap: 8px;
  max-height: 58vh;
  overflow-y: auto;
  padding: 2px;
}
.pile-grid .card {
  position: relative;
  width: 100%;
  transform: none;
}
.pile-grid .card.tapped {
  transform: none;
}

/* --- Context menu ------------------------------------------------------- */

.context-menu {
  position: fixed;
  z-index: 2000;
  min-width: 178px;
  padding: 4px;
  border: 1px solid var(--line-bright);
  border-radius: 7px;
  background: var(--panel-2);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.65);
}
.context-menu[hidden] {
  display: none;
}
.context-menu button {
  display: block;
  width: 100%;
  padding: 5px 9px;
  border: 0;
  border-radius: 4px;
  background: none;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.context-menu button:hover {
  background: var(--accent-dim);
}
.context-menu hr {
  margin: 4px 2px;
  border: 0;
  border-top: 1px solid var(--line);
}

/* Colour picker. Selectors are doubled up on .context-menu so they outrank the
   generic full-width button rule above. */
.context-menu .swatches {
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 5px;
  padding: 5px 6px 7px;
}

.context-menu .swatch {
  width: 24px;
  height: 24px;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 4px;
  cursor: pointer;
  transition: transform 0.1s;
}
.context-menu .swatch:hover {
  transform: scale(1.14);
}
.context-menu .swatch.active {
  box-shadow: 0 0 0 2px var(--panel-2), 0 0 0 3.5px #fff;
}
/* A colour another seat is already using. Still selectable — just marked. */
.context-menu .swatch.taken::after {
  content: "";
  display: block;
  width: 5px;
  height: 5px;
  margin: 2px 0 0 2px;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35);
}

.context-menu .swatch-custom {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 3px 9px 6px;
  color: var(--text-dim);
}
.context-menu .swatch-custom input[type="color"] {
  width: 38px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--line-bright);
  border-radius: 4px;
  background: none;
  cursor: pointer;
}
.context-menu .menu-heading {
  padding: 4px 9px 2px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}

/* --- Scry ---------------------------------------------------------------- */

/* Left to right is the top of the deck downwards, and the order the kept cards go back
   in. Cards headed for the bottom stay in the row rather than moving to a separate pile,
   so you can change your mind without hunting for where the card went. */
.scry-cards {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: center;
  padding: 6px 2px 2px;
}

.scry-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}

.scry-card .card {
  position: relative;
  width: calc(var(--card-w) * 1.6);
  transform: none;
  cursor: pointer;
  transition: opacity 0.12s, outline-color 0.12s, filter 0.12s;
  outline: 2px solid transparent;
}
.scry-card .card:hover {
  outline-color: var(--accent);
}

/* Going under: dimmed and turned down, so a glance along the row tells you what you
   decided without reading any of the labels. */
.scry-card.to-bottom .card {
  opacity: 0.45;
  filter: grayscale(0.7);
}

.scry-controls {
  display: flex;
  align-items: center;
  gap: 4px;
}

.scry-move {
  min-width: 26px;
  padding: 2px 6px;
}
.scry-move:disabled {
  opacity: 0.3;
  cursor: default;
}

.scry-where {
  min-width: 68px;
  padding: 2px 8px;
}
.scry-card.to-bottom .scry-where {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
}

/* A number and the buttons that act on it, inside a menu. */
.menu-row {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 3px 6px;
}
.menu-number {
  width: 52px;
  padding: 3px 6px;
  border: 1px solid var(--line-bright);
  border-radius: 4px;
  background: var(--panel-2);
  color: var(--text);
  font: inherit;
}
.menu-row button {
  flex: 1;
  padding: 3px 8px;
}

/* Greyed out rather than missing, so the row keeps its shape wherever it is opened and a
   pile that will not move looks like one. Without this a dead button still lights up
   under the pointer and reads as a control that is being ignored. */
.context-menu button:disabled {
  color: var(--muted);
  cursor: default;
}
.context-menu button:disabled:hover {
  background: none;
}

/* The two arrows read as a pair rather than as two more items in the list. */
.menu-arrange button {
  text-align: center;
}

/* Surveil sends cards to the graveyard rather than under the deck, so the marked ones
   read as gone rather than as merely reordered. */
.scry-card.to-bottom .scry-where {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
}

/* --- Turn steps ----------------------------------------------------------
   Solo mode only. Hidden entirely otherwise, so a four-player table is exactly the
   board it was before this existed. */

.steps {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "now   next"
    "track next";
  align-items: center;
  column-gap: 16px;
  row-gap: 5px;
  padding: 8px 14px 9px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}

/* Where you are, said in words. The chips underneath are the map; this is the sentence,
   and it is the part you read while actually playing. */
.step-now {
  grid-area: now;
  min-width: 0;
}
.step-now-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
}
.step-turn {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
}
.step-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
}
.step-says {
  font-size: 12px;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A display of its own beats the browser's rule for the hidden attribute, so say it
   here as every other component that hides itself already does. Without this the bar
   is laid out even when it is off — a strip of nothing at a four-player table, and the
   whole board row when it is on. */
.steps[hidden] {
  display: none;
}

.step-track {
  grid-area: track;
  display: flex;
  align-items: center;
  gap: 3px;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: thin;
}

/* One heading per phase rather than a tag on every chip: "Combat" said four times over
   is noise, said once is a heading. */
.step-phase {
  margin: 0 6px 0 10px;
  color: var(--muted);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
}
.step-phase:first-child {
  margin-left: 0;
}

.step-chip {
  padding: 3px 9px;
  border: 1px solid var(--line-bright);
  border-radius: 999px;
  background: var(--panel-2);
  color: var(--muted);
  font: 11px/1.5 ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.12s, color 0.12s, border-color 0.12s;
}
.step-chip:hover {
  color: var(--text);
  border-color: var(--accent-dim);
}

/* Where you have already been, so a glance says how far through the turn you are. */
.step-chip.past {
  opacity: 0.5;
}

.step-chip.now {
  background: color-mix(in srgb, var(--accent) 22%, var(--panel-2));
  border-color: var(--accent);
  color: var(--text);
  font-weight: 600;
}

/* Named for what it does rather than labelled "next", and sized so that confirming a
   phase is a deliberate press. */
.step-next {
  grid-area: next;
  align-self: stretch;
  min-width: 150px;
  padding: 8px 18px;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
}

/* Where each part of the page sits. Explicit, so nothing depends on source order. */
.topbar { grid-row: 1; }
.steps { grid-row: 2; }
.table { grid-row: 3; }
.hand-area { grid-row: 4; }

/* A line somebody said, rather than something the game did. The log is mostly
   bookkeeping scrolling past; anything addressed to the table has to stand out of it or
   it may as well not have been said. */
.log li.chat {
  padding: 2px 6px;
  margin: 2px 0;
  border-left: 2px solid currentColor;
  border-radius: 0 3px 3px 0;
  background: color-mix(in srgb, currentColor 10%, transparent);
  font-weight: 500;
}

/* A menu item that destroys something. Rare enough that it should not look like the
   others, and it is always paired with a line saying what it costs. */
.context-menu button.danger {
  color: var(--danger);
}
.context-menu button.danger:hover {
  background: color-mix(in srgb, var(--danger) 18%, transparent);
}

/* --- Getting started -----------------------------------------------------
   The opening of a game, one step at a time. Sat over the middle of the board because
   there is nothing on the board yet worth looking at, and because a panel you have to go
   hunting for is exactly the problem this is here to solve. Solo mode only. */

.setup {
  position: absolute;
  left: 50%;
  top: 46%;
  transform: translate(-50%, -50%);
  z-index: 500;
  width: min(440px, calc(100% - 40px));
  padding: 20px 24px 22px;
  border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
  border-radius: 12px;
  background: rgba(8, 11, 15, 0.97);
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.75);
  color: var(--text);
}
.setup[hidden] {
  display: none;
}

.setup-title {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 14px;
}

.setup-steps {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0 0 14px;
  padding: 0;
  list-style: none;
}

.setup-step {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border-radius: 6px;
  font-size: 14px;
  color: var(--muted);
}

/* The one you are on. Everything else is deliberately quiet so there is never a question
   about which line to read. */
.setup-step.now {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--text);
  font-weight: 600;
}
.setup-step.past {
  color: var(--text-dim);
}

.setup-mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  border: 1px solid var(--line-bright);
  border-radius: 50%;
  font-size: 11px;
  font-weight: 700;
}
.setup-step.now .setup-mark {
  border-color: var(--accent);
  background: var(--accent);
  color: #1a1408;
}
.setup-step.past .setup-mark {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--accent);
}

.setup-label {
  flex: 1;
}

.setup-note {
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.setup-says {
  margin: 0 0 16px;
  padding: 0 8px;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-dim);
}

.setup-actions {
  display: flex;
  gap: 10px;
}

/* Big, because this is the one thing to press and it should not need finding. */
.setup-go {
  flex: 1;
  padding: 11px 16px;
  font-size: 15px;
  font-weight: 600;
}

.setup-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}
.setup-head .setup-title {
  margin-bottom: 0;
}

/* A panel that appears on its own has to be dismissible. This is a suggestion about how
   to start, not a gate — ignoring the whole thing and playing is allowed. */
.setup-hide {
  padding: 2px 8px;
  border: 1px solid var(--line-bright);
  border-radius: 4px;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 11px;
  cursor: pointer;
}
.setup-hide:hover {
  color: var(--text);
  border-color: var(--accent-dim);
}

/* --- What the opponent is saying -----------------------------------------
   A practice opponent narrating into the log is narrating into a panel nobody is looking
   at. Combat especially is read off the board, so its last line is drawn across its own
   half of the table at a size that does not need finding. */

/* The rectangle of somebody's half of the table, laid over it as a sibling of the cards
   rather than inside the quadrant — see renderSpeech for why that matters. */
.speech-area {
  position: absolute;
  z-index: 400;
  /* The area lets clicks through to the board; only the panel itself takes them. */
  pointer-events: none;
  display: grid;
  place-items: center;
  padding: 12px;
}

.speech {
  /* Clickable: it has to be possible to be rid of it. */
  pointer-events: auto;
  cursor: pointer;
  max-width: min(760px, 94%);
  padding: 18px 28px;
  border: 1px solid color-mix(in srgb, var(--seat-color) 50%, transparent);
  border-left: 5px solid var(--seat-color);
  border-radius: 4px 12px 12px 4px;
  background: rgba(6, 9, 12, 0.95);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.8);
  color: var(--text);
  /* Written a line per matchup and drawn that way. */
  white-space: pre-line;
  /* Across the middle of its own half of the table and big enough to read from a chair
     you are not leaning out of. Which creature blocked which is the thing you came here
     to find out, so it gets the size that says so. */
  font-size: clamp(18px, 2.1vw, 32px);
  font-weight: 600;
  line-height: 1.5;
  text-align: center;
  text-wrap: balance;
  animation: speech-in 0.22s ease-out;
}

/* The heading line of a combat, so the pairs underneath read as a list rather than a
   paragraph. It is the first line and nothing else is shaped like it. */
.speech::first-line {
  font-size: 0.55em;
  font-weight: 700;
  letter-spacing: 0.18em;
  color: var(--seat-color);
}

@keyframes speech-in {
  from {
    opacity: 0;
    transform: scale(0.96);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* --- Triggers waiting ----------------------------------------------------
   Not a stack. Sanctum knows nothing about what a trigger does — only that somebody has
   said one is happening and the turn should not walk past it. Across the middle of the
   whole board because it concerns everybody, and because it is meant to be in the way. */

.triggers {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 600;
  display: flex;
  flex-direction: column;
  gap: 10px;
  width: min(560px, calc(100% - 40px));
  padding: 18px 20px;
  border: 1px solid var(--accent);
  border-radius: 12px;
  background: rgba(10, 8, 4, 0.97);
  box-shadow:
    0 0 0 4px color-mix(in srgb, var(--accent) 14%, transparent),
    0 20px 60px rgba(0, 0, 0, 0.85);
}
.triggers[hidden] {
  display: none;
}

.trigger-head {
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
}

.trigger-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px;
  border-left: 4px solid var(--seat-color);
  border-radius: 4px 8px 8px 4px;
  background: rgba(255, 255, 255, 0.04);
}

/* The card that triggered, so there is no doubt which one is meant. */
.trigger-art {
  position: relative;
  display: block;
  flex: none;
  width: 58px;
  aspect-ratio: 488 / 680;
  border-radius: 4px;
}
.triggers .trigger-art .card {
  position: absolute;
  inset: 0;
  width: 100%;
  transform: none;
  box-shadow: none;
  cursor: default;
}

.trigger-words {
  flex: 1;
  min-width: 0;
}

.trigger-name {
  font-size: 17px;
  font-weight: 600;
  color: var(--text);
}

.trigger-note {
  font-size: 12px;
  color: var(--muted);
}

.trigger-done {
  flex: none;
  padding: 9px 18px;
  font-size: 14px;
  font-weight: 600;
}

.trigger-all {
  align-self: flex-end;
}

/* A player's life inside a menu: the name and total, then the steppers. */
.menu-life-label {
  flex: 1;
  min-width: 0;
  padding: 0 2px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.menu-row button {
  min-width: 34px;
}

/* --- A locked table ------------------------------------------------------
   Shown over everything, because somebody arriving on a half-copied link has never seen
   this screen and will not think to look in a log panel for the reason. */

.locked {
  position: fixed;
  inset: 0;
  z-index: 5000;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(6, 9, 12, 0.96);
}
.locked[hidden] {
  display: none;
}

.locked-card {
  max-width: 460px;
  padding: 28px 32px;
  border: 1px solid color-mix(in srgb, var(--danger) 55%, transparent);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8);
  text-align: center;
}

.locked-title {
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--danger);
  margin-bottom: 12px;
}

.locked-why {
  margin: 0 0 14px;
  font-size: 15px;
  line-height: 1.5;
  color: var(--text);
}

.locked-hint {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--muted);
}
.locked-hint code {
  padding: 1px 5px;
  border-radius: 3px;
  background: var(--panel-2);
  color: var(--text);
}
