/**
 * DUEL — the arena, seen from a perch above it.
 *
 * The rest of the site is the Generator's control panel. The duel is the one
 * place you are not looking at an instrument: you are looking down into a real
 * arena floor, set into a machined tray in the console. So the floor is dirt
 * and scored stone, the frame around it is hull and brass, and the only lit
 * things are the lamps that mark where a piece may go — plus the creatures
 * themselves, which keep their element colour.
 *
 * Everything here is on the --g-* scale. This file used to run on the --x-*
 * shim, which was the last thing keeping tokens.css alive.
 */

/* -------------------------------------------------------------------------
   THE ROOM
   ------------------------------------------------------------------------- */

/* The ground behind the tray. The per-client tint (which side you are) is
   applied inline from duelBoard.js; this is the ground it sits on. */
.duel-page-background-overlay {
  position: fixed;
  top: 0;
  left: 0;
  background: var(--g-void);
  height: 100%;
  width: 100%;
}

/* The legend over the board: whose view this is. */
.duel-view-label {
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-small);
  font-weight: 600;
  letter-spacing: var(--g-track-wide);
  text-transform: uppercase;
  color: var(--g-ink-low);
  text-align: center;
  margin: 0;
}

.duel-winner-text {
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-h2);
  font-weight: 600;
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
  color: var(--g-ink);
  text-align: center;
  margin: auto;
}

/* -------------------------------------------------------------------------
   THE TRAY
   ------------------------------------------------------------------------- */

/* A machined tray bolted into the console, with the arena floor recessed into
   it. Was a black box with a 10% radius floating in its own glow. */
.duel-board-wrapper {
  width: fit-content;
  margin: min(5%, 5px) auto;
  padding: min(4%, 18px);
  position: relative;
  border-radius: var(--g-radius-housing);
  background: linear-gradient(160deg, var(--g-hull-hi), var(--g-hull));
  border: 2px solid var(--g-brass-dark);
  box-shadow: var(--g-relief), var(--g-drop);
  /* four fasteners, drawn rather than marked up */
  background-image:
    radial-gradient(circle at 12px 12px, rgba(0, 0, 0, 0.6) 1.5px, var(--g-brass-dark) 2.4px, var(--g-bevel-light) 3px, transparent 3.6px),
    radial-gradient(circle at calc(100% - 12px) 12px, rgba(0, 0, 0, 0.6) 1.5px, var(--g-brass-dark) 2.4px, var(--g-bevel-light) 3px, transparent 3.6px),
    radial-gradient(circle at 12px calc(100% - 12px), rgba(0, 0, 0, 0.6) 1.5px, var(--g-brass-dark) 2.4px, var(--g-bevel-light) 3px, transparent 3.6px),
    radial-gradient(circle at calc(100% - 12px) calc(100% - 12px), rgba(0, 0, 0, 0.6) 1.5px, var(--g-brass-dark) 2.4px, var(--g-bevel-light) 3px, transparent 3.6px),
    linear-gradient(160deg, var(--g-hull-hi), var(--g-hull));
}

/* The arena floor. Was a hotlinked JPG from a blogspot page — a live
   production dependency on somebody's blog. This is grain and scoring
   generated in CSS: warm dust over dark stone, with the faint scoured arcs a
   floor gets from things being dragged across it. */
.duel-board-wrapper-background {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 100%;
  border-radius: var(--g-radius);
  background-color: #1b1710;
  /* dust and scouring, not corduroy: the marks have to be visible only when
     you look for them, or the floor competes with the pieces standing on it */
  background-image:
    repeating-linear-gradient(58deg, rgba(255, 244, 214, 0.014) 0 1px, transparent 1px 23px),
    repeating-linear-gradient(-31deg, rgba(0, 0, 0, 0.16) 0 1px, transparent 1px 31px),
    radial-gradient(ellipse at 28% 22%, rgba(176, 141, 63, 0.05) 0%, transparent 55%),
    radial-gradient(ellipse at 74% 76%, rgba(176, 141, 63, 0.035) 0%, transparent 50%),
    radial-gradient(ellipse at 62% 34%, rgba(0, 0, 0, 0.28) 0%, transparent 45%);
}

/* Vignette: the tray is deep, so the floor darkens toward its walls. */
.duel-board-wrapper-background-overlay {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 100%;
  border-radius: var(--g-radius);
  background:
    radial-gradient(circle at 50% 45%, rgba(0, 0, 0, 0) 35%, rgba(0, 0, 0, 0.72) 100%);
  box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.9), inset 0 -1px 0 var(--g-bevel-light);
}

/* -------------------------------------------------------------------------
   THE GRID
   ------------------------------------------------------------------------- */

/* Scored into the floor: a dark cut with the light catching its upper lip. */
.duel-board-cell-connector {
  background-color: rgba(0, 0, 0, 0.7);
  box-shadow: 0 1px 0 rgba(255, 244, 214, 0.05);
  position: absolute;
  transform: translate(-50%, -50%) !important;
  /* deep enough to read as a cut floor, shallow enough that the pieces
     standing on it stay the loudest thing in the tray */
  opacity: 0.45;
  pointer-events: none;
  z-index: 1;
}

/* WHERE YOU MAY GO.
   One traced outline over the whole reachable area rather than a border per
   square. Built per cell it could round its convex corners and nothing else: a
   concave corner is drawn by two different cells meeting at right angles, and
   neither can shorten itself to leave room for a curve, so the arms of a cross
   ended soft while the joints between them stayed sharp. */
.duel-board-grid {
  position: relative;
  width: fit-content;
}

/* The table carried two pixels of border-spacing, so the squares were not
   actually contiguous and the grid measured fourteen pixels wider than eight
   cells. Nothing wanted that gap - the scoring in the floor is drawn inside the
   cells - and an overlay cannot align to a grid whose geometry is a lie. */
#board,
#board tbody,
#board tr,
#board td {
  border-collapse: collapse;
  border-spacing: 0;
  padding: 0;
  border: 0;
}

.duel-move-layer {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  z-index: 6;
  overflow: visible;
}

.duel-move-shape {
  fill: rgba(255, 176, 55, 0.055);
  stroke: rgba(255, 176, 55, 0.42);
  stroke-width: 1.5;
  stroke-linejoin: round;
}

/* the mark on each square you may actually land on */
.duel-move-pip {
  fill: var(--g-lamp-amber);
  opacity: 0.7;
}

/* Where a piece you are only inspecting could go. An outline with no fill is a
   different statement, not a weaker one - the old version was the same mark at
   lower opacity, which only read as "dimmer for reasons unknown". */
.duel-move-layer--referenced .duel-move-shape {
  fill: none;
  stroke: rgba(221, 212, 189, 0.26);
  stroke-dasharray: 4 4;
}

.duel-move-layer--referenced .duel-move-pip {
  fill: var(--g-ink-mid);
  opacity: 0.35;
}

.duel-cell-style-covered {
  pointer-events: none;
  position: absolute;
}

/* -------------------------------------------------------------------------
   PIECES
   ------------------------------------------------------------------------- */

/* Each creature stands on a token base — a machined disc with a brass rim, so
   a piece reads as an object standing on the floor rather than as a sticker
   printed on it. */
.duel-piece-base {
  /* THE PLATE - the square a creature is standing in, and the carrier of every
     state on this board.
     Was a 70%-wide ellipse pinned to the bottom of the square: the ground shadow
     a creature drawn in elevation needs. With creatures seated in their own
     squares that ellipse cut a line through every body.
     At rest it draws nothing but a soft contact shadow. An outline here would
     put a box around all twelve pieces at all times, which turns a board of
     creatures into a board of tiles and spends the one mark that ought to mean
     "something is true of this square". */
  position: absolute;
  inset: 1px;
  border-radius: 2px;
  background: radial-gradient(ellipse at 50% 92%, rgba(0, 0, 0, 0.55) 0%, transparent 62%);
  border: 1px solid transparent;
  opacity: 1;
  z-index: 99;
  pointer-events: none;
}

/* THE CREATURE SITS IN ITS SQUARE.
   This was bottom: 35% on a 95%-tall image, which pushed roughly a fifth of
   every creature up out of the square it occupies. That one line was the reason
   three separate attempts at a floor mark all read as "a sticker under a
   creature": the floor was drawn in plan and the creature in elevation, so no
   mark on the floor could ever appear to contain it.

   The replacement is not just that shift deleted. Two things decide where a
   creature sits, and both are deliberate:

   1. The box is square and centred horizontally. Species art is authored at
      roughly 900x985 in its own viewBox and preserveAspectRatio letterboxes it
      to fit, so a box that is not square shifts every creature by a different
      amount depending on how tall its art happens to be. Percentage insets on
      each edge separately gave 4px of margin at the top and 3px at the bottom -
      close enough to look centred and not actually centred.

   2. It is nudged down, not centred, and by a known amount. Art fills its
      viewBox to the edges, so the bottom of the ink is the bottom of the box: a
      centred box floats the creature a hair above the square's own floor. One
      pixel of lift reads as standing; more reads as hovering. inset with a
      larger bottom value is the whole adjustment, and it is stated in pixels so
      it does not scale into a shrug on a small board. */
.duel-piece-xalian-icon {
  position: absolute;
  /* The wrapper carries aspect-ratio: 1, so pinning all four edges
     over-constrains it - the height wins and the art hangs out of the square.
     Width and height are both given as the same percentage of a square cell, so
     the box is square by construction rather than by two different percentages
     that happen to agree at one width.

     Anchored to the bottom rather than centred. Species art fills its own
     viewBox to the edges, so the foot of the ink is the foot of the box, and a
     centred box floats every creature a little above the floor of the square it
     is standing in. */
  left: 5%;
  bottom: 2%;
  width: 90%;
  height: 90%;
  padding: 0px !important;
  z-index: 101;
}

.duel-attack-action-xalian {
  height: 100%;
  width: 100%;
  padding: 0px !important;
}

.duel-stat-chart {
  height: 200px;
}

/* The element discs pinned to a piece: bulbs behind coloured plastic, seated
   in a metal ring. */
.duel-type-badge {
  position: absolute;
  bottom: 0;
  left: -5%;
  z-index: 105;
  border-radius: 50%;
  border: 1px solid var(--g-seam);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), inset 0 1px 0 var(--g-bevel-light);
  text-align: center;
}

.type-badge {
  border-radius: 50%;
  border: 1px solid var(--g-seam);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.9), inset 0 1px 0 var(--g-bevel-light);
  text-align: center;
  margin: auto;
}

.duel-type-attack-badge {
  position: absolute;
  top: 0;
  right: 0;
  border-radius: 50%;
  border: 1px solid var(--g-seam);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
  text-align: center;
  z-index: 105;
}

.duel-type-badge-symbol {
  padding: 2%;
  margin: auto !important;
}

.duel-xalian-cell-badge-row {
  width: 100%;
  margin: auto !important;
  position: absolute;
  bottom: 0;
  left: 0;
}

/* -------------------------------------------------------------------------
   TARGETS AND FLAGS
   ------------------------------------------------------------------------- */

/* WHAT YOU MAY STRIKE.
   Reach is a relationship between two pieces, so the mark is one too: a firing
   solution struck across the floor from the attacker, and the target's own tile
   marked out with registration ticks in the grid's own geometry. Every earlier
   attempt decorated the target instead - icons on its face, a ring round its
   body, an ellipse round its plinth - and decoration of one object can never
   say anything about a pair of them. A ring concentric with a piece's base is
   just a second base. */
.duel-target-layer {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  z-index: 7;
  overflow: visible;
}

/* Movement is permission and a target is a warning, so they are different
   channels rather than two weights of one. Amber marks the ground you may take;
   the warning lamp marks what you may strike. Hazard gold sits too close to the
   movement amber to carry this - the two marks overlap constantly, and at a
   glance they were the same colour. */
.duel-solution {
  stroke: var(--g-lamp-red);
  stroke-width: 1.5;
  stroke-dasharray: 5 4;
  opacity: 0.8;
}

/* the target's square, lifted just enough to separate it from the floor */
.duel-target-wash {
  fill: var(--g-lamp-red);
  opacity: 0.07;
}

/* The bracket's backing plate.
   The idea was a groove: a dark stroke behind a lit one, reading as a cut with
   light on its lip. On a floor this dark that buys a one-pixel black halo
   against near-black - depth you can measure and cannot see. What actually
   separates the mark from the floor is the opposite: a warm metal edge, as if
   the bracket were a brass inlay set into the stone rather than a line drawn
   over it. That reads at any size and belongs to the same hull vocabulary as
   every other machined thing on this board. */
.duel-target-groove {
  fill: none;
  stroke: color-mix(in srgb, var(--g-brass-dark) 70%, black);
  stroke-width: 5;
  stroke-linecap: square;
}

.duel-target-ticks {
  fill: none;
  stroke: var(--g-lamp-red);
  stroke-width: 2;
  stroke-linecap: square;
  opacity: 0.95;
}

/* The figure is set in viewBox units - one unit is one square - so it scales
   with the board rather than needing a size per breakpoint. At 0.19 it came out
   around eleven pixels on a full board, which is below the size this typeface
   stays legible at over a textured floor. */
.duel-target-figure {
  font-family: var(--g-font-mono);
  font-size: 0.26px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  fill: var(--g-lamp-red);
  paint-order: stroke;
  stroke: rgba(0, 0, 0, 0.85);
  stroke-width: 0.05px;
}

/* One hue, three intensities. A matchup worth spending your single attack on
   is drawn at full strength; one that is not recedes to unlit metal, so a bad
   trade reads as "not this one" without having to be decoded. */
.duel-target-tile--super .duel-target-ticks,
.duel-target-tile--strong .duel-target-ticks { opacity: 1; stroke-width: 2.75; }

.duel-target-tile--super .duel-target-groove,
.duel-target-tile--strong .duel-target-groove { stroke-width: 4.75; }

.duel-target-tile--super .duel-target-wash,
.duel-target-tile--strong .duel-target-wash { opacity: 0.11; }

.duel-target-tile--weak .duel-target-ticks,
.duel-target-tile--immune .duel-target-ticks { stroke: var(--g-ink-mid); stroke-width: 1.75; opacity: 0.85; }

.duel-target-tile--weak .duel-target-groove,
.duel-target-tile--immune .duel-target-groove { stroke-width: 3.5; }

.duel-target-tile--weak .duel-target-figure,
.duel-target-tile--immune .duel-target-figure { fill: var(--g-ink-mid); }

.duel-target-tile--weak .duel-target-wash,
.duel-target-tile--immune .duel-target-wash { fill: var(--g-ink-low); opacity: 0.05; }

/* COMMANDED.
   Not a box. Two earlier passes drew a square around the creature and then tried
   to make the square less objectionable - thinner, then with its edge animated -
   but the objection was the square. A rectangle enclosing a creature says
   "container", and nothing about being the piece under your hand is containment.

   The console has acquired this specimen. Four corner brackets fly in from
   outside and lock on, the way an instrument acquires a target, then hold with a
   slow breath so the mark stays alive without moving enough to distract. The
   brackets never join, so there is no outline - the corners imply the square and
   the creature stands in open space. */
.duel-piece--selected .duel-piece-base {
  border: 0;
  background: radial-gradient(ellipse at 50% 92%, rgba(0, 0, 0, 0.55) 0%, transparent 62%);
  box-shadow: none;
}

/* Four corners, painted as eight gradient arms on one element. Two
   pseudo-elements can only honestly draw two corners; the box-shadow trick meant
   to fake the other two drew filled squares rather than brackets. As a
   background every corner has the same construction and there is one thing to
   animate. */
.duel-piece--selected .duel-piece-base::before {
  content: '';
  position: absolute;
  inset: -2px;
  pointer-events: none;
  background:
    linear-gradient(var(--duel-team), var(--duel-team)) 0 0 / 30% 2px no-repeat,
    linear-gradient(var(--duel-team), var(--duel-team)) 0 0 / 2px 30% no-repeat,
    linear-gradient(var(--duel-team), var(--duel-team)) 100% 0 / 30% 2px no-repeat,
    linear-gradient(var(--duel-team), var(--duel-team)) 100% 0 / 2px 30% no-repeat,
    linear-gradient(var(--duel-team), var(--duel-team)) 0 100% / 30% 2px no-repeat,
    linear-gradient(var(--duel-team), var(--duel-team)) 0 100% / 2px 30% no-repeat,
    linear-gradient(var(--duel-team), var(--duel-team)) 100% 100% / 30% 2px no-repeat,
    linear-gradient(var(--duel-team), var(--duel-team)) 100% 100% / 2px 30% no-repeat;
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--duel-team) 65%, transparent));
  animation:
    duel-acquire 260ms cubic-bezier(0.16, 0.84, 0.36, 1) both,
    duel-hold 2.6s ease-in-out 260ms infinite;
}

/* fly in from outside and lock on */
@keyframes duel-acquire {
  from { opacity: 0; transform: scale(1.75); }
  to   { opacity: 1; transform: scale(1); }
}

/* the held breath - a few percent, slowly, so it reads as alive rather than as
   something demanding to be watched */
@keyframes duel-hold {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.06); }
}

@media (prefers-reduced-motion: reduce) {
  .duel-piece--selected .duel-piece-base::before {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

/* The pool of light the energised plinth casts on the floor.
   This was drawn at z-index 98, underneath the token base at 100 - and the base
   is a 95%-opaque disc sitting on exactly the spot the glow was centred on, so
   the light was painted behind the only thing it could have lit and the selected
   piece read as at rest. It spills wider than the disc now (the disc is 70% of
   the square, the pool is 96%) so what shows is the floor around the plinth
   rather than the plinth itself, and it sits above the base so that ring of
   floor is actually lit. */
/* The pool of light a plinth cast on the floor around it. With the plate now
   being the square, the lit tile is that pool - a second glow on top of it was
   the halo this design has been trying to get rid of. */
.duel-piece--selected::after {
  content: none;
}

/* a piece you are only inspecting: the plinth is lit, but not in a side's
   colour, because nothing is being commanded */
/* a piece you are only inspecting: the tile is lit, but in no side's colour,
   because nothing is being commanded */
/* Inspecting is a question, not a command, so the tile is outlined rather than
   filled - a dashed edge and no wash. Filled in bone it read as a lit tile in a
   colour no side owns, which is a fourth state nobody asked for. */
.duel-piece--referenced .duel-piece-base {
  border-color: rgba(221, 212, 189, 0.45);
  border-style: dashed;
  background: radial-gradient(ellipse at 50% 92%, rgba(0, 0, 0, 0.55) 0%, transparent 62%);
}

.duel-piece--referenced::after {
  content: none;
}

/* VITALS ON THE BOARD.
   These live in the roster rail now. On a piece they are a detail you ask for
   by pointing at it, so twelve pairs of two-pixel bars are not competing with
   the board at all times. */
/* Vitals float above the creature rather than sitting inside its square, and
   they are sized in pixels rather than in percentages of a cell. Anything scaled
   to the square shrinks to nothing on a small board, which is exactly how the
   old two-pixel bars happened. */
.duel-piece-vitals {
  position: absolute;
  left: 50%;
  bottom: 88%;
  transform: translateX(-50%);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--g-fast);
  z-index: 620;
}

.duel-vitals-plate {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px 5px;
  border-radius: var(--g-radius-sm);
  background: rgba(16, 14, 10, 0.94);
  border: 1px solid var(--g-brass-dark);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.9);
  line-height: 1;
}

.duel-vitals-row {
  display: flex;
  align-items: center;
  gap: 4px;
}

.duel-vitals-figure {
  font-family: var(--g-font-mono);
  font-size: 11px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--g-ink-high);
  min-width: 20px;
  text-align: right;
}

.duel-vitals-row--stamina .duel-vitals-figure {
  color: var(--g-ink-mid);
}

.duel-vitals-track {
  display: block;
  position: relative;
  width: 34px;
  height: 4px;
  border-radius: 1px;
  background: var(--g-seam);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.9);
  overflow: hidden;
}

.duel-vitals-fill {
  display: block;
  height: 100%;
}

/* Hover and focus only. When vitals were a two-pixel bar tucked under the token
   it cost nothing to leave them up on the selected and referenced pieces; now
   that they are a plate floating above the creature, doing so would park a panel
   over the square behind whichever piece you are commanding - which is usually
   the square you are trying to read. The rail carries the standing readout. */
.duel-board-cell:hover .duel-piece-vitals,
.duel-board-cell:focus-within .duel-piece-vitals {
  opacity: 1;
}

/* FLAGS.
   A flag standing on the floor, and a flag being carried. The third state the
   old code drew (guarding) had been commented out of the render long enough to
   count as dead, and three meanings from one rotated icon was already one too
   many. */
.duel-flag {
  z-index: 410;
  position: absolute;
  top: -20%;
  left: 20%;
  opacity: 0.9;
  height: 60%;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.9));
}

/* Carrying the enemy flag wins the game outright, so a carrier is the loudest
   piece on the board: a pennant plus a ring around its base in the flag colour. */
/* A CARRIED FLAG.
   This was a pennant at 58% of the creature's height, hung outside the token box
   at top right, floating in the air beside the body with nothing joining the two.
   It was the most literal sticker on the board.

   A carried flag is now a standard planted on the plate: smaller, tucked against
   the creature's flank, its foot on the disc rather than its head in the sky, so
   the creature is holding something rather than having something hovering near
   it. The loud part of the state moved to the plate, which is where every other
   state on this board lives. */
.duel-flag-carried {
  /* in front of the creature (101) and its plate (100): it is being held, not
     standing behind the thing holding it */
  z-index: 103;
  position: absolute;
  bottom: 8%;
  right: 2%;
  height: 46%;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.95));
}

/* The plate a carrier stands on is hazard-striped in the flag's colour. Carrying
   is the win condition, so it gets the loudest treatment the floor has - and
   because it is on the plate rather than on the creature, it stacks with
   selection instead of fighting it. */
.duel-piece--carrying .duel-piece-base {
  border-color: color-mix(in srgb, var(--duel-flag, var(--g-brass)) 85%, white);
  border-width: 2px;
  background:
    repeating-linear-gradient(
      -45deg,
      color-mix(in srgb, var(--duel-flag, var(--g-brass)) 42%, transparent) 0 4px,
      transparent 4px 9px),
    linear-gradient(160deg, rgba(255, 244, 214, 0.05) 0%, transparent 45%);
  box-shadow:
    inset 0 1px 0 rgba(255, 244, 214, 0.14),
    0 0 12px color-mix(in srgb, var(--duel-flag, var(--g-brass)) 55%, transparent);
}

/* -------------------------------------------------------------------------
   DETAIL PANELS
   ------------------------------------------------------------------------- */

/* the hover gauge, sat on the token base */
.duel-state-chart-wrapper-position {
  position: absolute;
  bottom: 5%;
  left: 50%;
  transform: translateX(-50%);
  width: 58%;
}

.duel-state-chart-wrapper-position > div > div > svg {
  vertical-align: unset;
}

.fit-text {
  text-align: center;
  margin: auto;
}

/* -------------------------------------------------------------------------
   STATUS STRIP
   ------------------------------------------------------------------------- */

/* The instruments for the turn, on a panel bolted along the bottom of the
   console. Was a black gradient bar with bootstrap buttons on it. */
.duel-status-strip {
  margin: 0 auto;
  max-width: 460px;
  padding: var(--g-3) var(--g-4) var(--g-4);
  background: var(--g-hull);
  border: var(--g-hairline) solid var(--g-seam);
  border-bottom: 0;
  border-radius: var(--g-radius-panel) var(--g-radius-panel) 0 0;
  box-shadow: var(--g-relief), 0 -4px 14px rgba(0, 0, 0, 0.6);
}

.duel-status-readouts {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  gap: var(--g-5);
  padding-bottom: var(--g-3);
  margin-bottom: var(--g-3);
  border-bottom: var(--g-hairline) solid var(--g-seam);
}

.duel-readout {
  text-align: center;
}

.duel-readout-label {
  display: block;
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-micro);
  font-weight: 600;
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
  color: var(--g-ink-low);
}

.duel-readout-value {
  display: block;
  font-family: var(--g-font-mono);
  font-size: var(--g-size-lead);
  font-variant-numeric: tabular-nums;
  color: var(--g-ink);
  line-height: 1.2;
}

/* the attack is either still in hand or it is spent */
.duel-readout-value--spent {
  color: var(--g-ink-low);
}

.duel-readout-value--held {
  color: var(--g-lamp-amber);
}

.duel-status-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--g-3);
}

.duel-status-stale {
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-micro);
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
  color: var(--g-lamp-red);
  text-align: center;
  margin: 0 0 var(--g-2) 0;
}

/* -------------------------------------------------------------------------
   DIALOGS
   The two dialogs the duel raises mid-match — pick a move, watch it land.
   They sit on the same housing as the rest of the console; nothing in them
   is a bootstrap default.
   ------------------------------------------------------------------------- */

/* a control dialog, not a page of prose: narrower than .themed-modal's
   reading measure. Both dialogs are shadcn Dialogs now (radix content is a
   single element, not modal-dialog/modal-content split), so these override
   the component's own defaults directly on the class it carries. */
.duel-modal {
  max-width: 460px !important;
}

.duel-modal [data-slot="dialog-title"] {
  font-size: var(--g-size-body) !important;
}

/* the verb between the two names is printed, not stencilled */
.duel-modal-title-joiner {
  font-family: var(--g-font-ui);
  font-weight: 400;
  text-transform: lowercase;
  letter-spacing: 0;
  color: var(--g-ink-low);
}

.duel-move-legend {
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-micro);
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
  color: var(--g-ink-low);
  margin: 0 0 var(--g-3) 0;
}

.duel-move-list {
  display: flex;
  flex-direction: column;
  gap: var(--g-2);
}

/* one firing solution: a recessed row you press */
.duel-move-option {
  display: grid;
  grid-template-columns: 34px 1fr auto;
  align-items: center;
  gap: var(--g-3);
  width: 100%;
  padding: var(--g-3);
  text-align: left;
  background: var(--g-hull-lo);
  border: var(--g-hairline) solid var(--g-seam);
  border-radius: var(--g-radius);
  box-shadow: var(--g-recess);
  transition: background var(--g-fast), border-color var(--g-fast);
}

.duel-move-option:hover,
.duel-move-option:focus-visible {
  background: var(--g-hull);
  border-color: var(--g-brass-dark);
  outline: none;
}

.duel-move-option:active {
  background: var(--g-seam);
}

/* the element plate the move is cut for */
.duel-move-plate {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 34px;
}

.duel-move-ident {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.duel-move-name {
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-body);
  font-weight: 500;
  letter-spacing: var(--g-track-tight);
  text-transform: uppercase;
  color: var(--g-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.duel-move-rating {
  font-family: var(--g-font-mono);
  font-size: var(--g-size-micro);
  font-variant-numeric: tabular-nums;
  color: var(--g-ink-low);
}

.duel-move-readout {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
}

/* the projected number, in machine type, the way every other readout on the
   console prints a figure */
.duel-move-damage {
  font-family: var(--g-font-mono);
  font-size: var(--g-size-lead);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  color: var(--g-lamp-amber);
}

.duel-move-effect {
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-micro);
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
}

.duel-move-cancel-row {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--g-4);
}

/* The blow landing. Fitty sizes the two headings to the dialog, so they set no
   size of their own — only face, tracking and colour. The shadcn DialogContent
   is one element playing the part of both modal-content and modal-body, so
   this overrides its own p-6 default directly. */
.duel-action-modal {
  padding: var(--g-5) !important;
}

.duel-action-heading {
  width: 90%;
  margin: 0 auto;
  padding: var(--g-2) 0;
}

.duel-action-title {
  font-family: var(--g-font-stencil);
  font-weight: 600;
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
  color: var(--g-ink);
  text-align: center;
  margin: 0;
}

/* the verdict — how well it worked */
.duel-action-effect {
  font-family: var(--g-font-stencil);
  font-weight: 500;
  letter-spacing: var(--g-track-wide);
  text-transform: uppercase;
  color: var(--g-lamp-amber);
  text-align: center;
  margin: 0;
}

/* each combatant's health, as the same dead-socket strip the pieces carry */
.duel-action-vitals {
  width: 100%;
  height: 3px;
  margin-top: 10%;
  border-radius: 1px;
  background: var(--g-seam);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.9);
  overflow: hidden;
}

.duel-action-damage {
  font-family: var(--g-font-mono);
  font-size: var(--g-size-lead);
  font-variant-numeric: tabular-nums;
  color: var(--g-lamp-red);
  text-align: center;
  margin: 0;
}

/* the column legends over the effectiveness discs */
.duel-effect-column-label {
  display: block;
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-micro);
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
  color: var(--g-ink-low);
  margin-bottom: var(--g-2);
}

/* the standfirst above the how-to sections */
.themed-modal .modal-body p.howto-standfirst {
  font-size: var(--g-size-small);
  color: var(--g-ink-mid);
  margin-bottom: var(--g-6);
}

.duel-modal-footer {
  border-top: var(--g-hairline) solid var(--g-seam);
  padding: var(--g-4) var(--g-5);
}


/* -------------------------------------------------------------------------
   THE STAGE
   A rail on each side, the arena between them. The benches used to sit above
   and below the board, which meant the board slid down the page the first time
   anything died. Nothing here moves once the match starts.
   ------------------------------------------------------------------------- */

.duel-viewport {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* the status strip is fixed to the bottom, so the stage reserves its height
     rather than letting the board's last rank slide underneath it */
  padding-bottom: 150px;
}

.duel-stage-shell {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  gap: var(--g-3);
  padding: var(--g-3) var(--g-4) 0;
  width: 100%;
}

.duel-stage-header {
  flex: 0 0 auto;
  text-align: center;
}

/* Whose turn it is. This was previously implied only by an edge glow on a
   bench, which is not something a player can actually read. */
.duel-turn-banner {
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-lead);
  font-weight: 600;
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
  margin: 0;
}

.duel-turn-banner--yours { color: var(--g-lamp-amber); }
.duel-turn-banner--theirs { color: var(--g-ink-low); }

.duel-stage {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: var(--g-4);
}

.duel-stage-board {
  flex: 0 0 auto;
  align-self: center;
  display: flex;
  justify-content: center;
}

/* Inside the stage the wrapper's auto margins fight the flex centring, and its
   percentage padding resolves against a width derived from the wrapper itself -
   so the padding fell outside the parent's measured box and pushed the
   right-hand rail under the board. Both are pinned here. */
.duel-stage-board .duel-board-wrapper {
  margin: 0;
  padding: 16px;
}

/* Below the breakpoint the rails lie down above and below the board and give
   their width back to it. */
.duel-stage--stacked {
  flex-direction: column;
  align-items: center;
  gap: var(--g-2);
  overflow-y: auto;
}

/* the opponent reads above the board, you read below it */
.duel-stage--stacked .duel-roster-rail--left { order: 1; }
.duel-stage--stacked .duel-stage-board { order: 2; }
.duel-stage--stacked .duel-roster-rail--right { order: 3; }

/* -------------------------------------------------------------------------
   THE ROSTER RAIL
   ------------------------------------------------------------------------- */

.duel-roster-rail {
  flex: 0 0 auto;
  width: 270px;
  max-height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--g-hull);
  border: var(--g-hairline) solid var(--g-seam);
  border-radius: var(--g-radius-panel);
  box-shadow: var(--g-relief), var(--g-drop);
  overflow: hidden;
}

/* the side to move carries an edge light along the rail, in its own colour */
.duel-roster-rail--active {
  border-color: color-mix(in srgb, var(--duel-team) 45%, var(--g-seam));
  box-shadow:
    var(--g-relief),
    var(--g-drop),
    inset 0 0 18px color-mix(in srgb, var(--duel-team) 18%, transparent);
}

.duel-roster-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--g-2);
  padding: var(--g-3) var(--g-4);
  border-bottom: var(--g-hairline) solid var(--g-seam);
  background: var(--g-hull-lo);
}

.duel-roster-title {
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-micro);
  font-weight: 600;
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
  color: var(--g-ink-mid);
}

/* how the match is going, which nothing used to say */
.duel-roster-count {
  font-family: var(--g-font-mono);
  font-size: var(--g-size-small);
  font-variant-numeric: tabular-nums;
  color: var(--g-ink);
}

.duel-roster-count-of { color: var(--g-ink-low); }

.duel-roster-entries {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: var(--g-2);
  display: flex;
  flex-direction: column;
  gap: var(--g-2);
}

/* one slot, held for the whole match so nothing reshuffles under the cursor */
.duel-roster-entry {
  display: grid;
  grid-template-columns: 34px 1fr;
  align-items: start;
  gap: var(--g-3);
  width: 100%;
  padding: var(--g-2);
  text-align: left;
  background: var(--g-hull-lo);
  border: var(--g-hairline) solid var(--g-seam);
  border-left: 2px solid var(--duel-team);
  border-radius: var(--g-radius);
  box-shadow: var(--g-recess);
  transition: background var(--g-fast), border-color var(--g-fast), opacity var(--g-fast);
}

.duel-roster-entry:hover,
.duel-roster-entry:focus-visible {
  background: var(--g-hull);
  outline: none;
}

.duel-roster-entry--selected {
  background: var(--g-hull);
  border-color: var(--g-lamp-amber);
  border-left-color: var(--g-lamp-amber);
}

.duel-roster-entry--referenced {
  border-color: var(--g-ink-low);
}

/* the enemy rail marks what your selection can reach, so the matchup can be
   read off the board entirely */
.duel-roster-entry--targetable {
  border-color: var(--g-hazard);
}

.duel-roster-entry--carrying {
  border-color: var(--g-lamp-amber);
  box-shadow: var(--g-recess), 0 0 10px rgba(255, 176, 55, 0.28);
}

/* still in the box during setup */
.duel-roster-entry--unset {
  opacity: 0.75;
  border-left-style: dashed;
}

/* out of the match: the slot stays put, struck through */
.duel-roster-entry--down {
  opacity: 0.4;
  border-left-color: var(--g-seam);
}

.duel-roster-entry--down .duel-roster-portrait-art {
  filter: grayscale(1);
}

.duel-roster-entry--down .duel-roster-name {
  text-decoration: line-through;
}

.duel-roster-portrait {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 34px;
  width: 34px;
}

.duel-roster-portrait-art {
  height: 100%;
  width: 100%;
}

.duel-roster-flag {
  position: absolute;
  top: -4px;
  right: -6px;
  height: 15px;
  fill: var(--g-lamp-amber);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.95));
}

.duel-roster-body {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.duel-roster-ident {
  display: flex;
  align-items: baseline;
  gap: var(--g-2);
  min-width: 0;
}

.duel-roster-name {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--g-font-stencil);
  font-size: var(--g-size-small);
  font-weight: 500;
  letter-spacing: var(--g-track-tight);
  text-transform: uppercase;
  color: var(--g-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.duel-roster-tag {
  flex: 0 0 auto;
  font-family: var(--g-font-mono);
  font-size: 9px;
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
  color: var(--g-ink-low);
}

.duel-roster-tag--place { color: var(--g-lamp-amber); }
.duel-roster-tag--down { color: var(--g-lamp-red); }

.duel-roster-tag--verdict {
  padding: 0 3px;
  border-radius: var(--g-radius-sm);
  border: 1px solid var(--g-hazard);
  color: var(--g-hazard);
}

.duel-roster-verdict--super,
.duel-roster-verdict--strong {
  border-color: var(--g-lamp-amber);
  color: var(--g-lamp-amber);
}

/* Matches the board's own weak/immune ink. Dead ink still has to be read - at
   ink-low over the rail's dark slot the multiplier was invisible, which turned
   "this trade is bad" into "there is nothing here". */
.duel-roster-verdict--weak,
.duel-roster-verdict--immune {
  border-color: var(--g-ink-mid);
  color: var(--g-ink-mid);
}

.duel-roster-meters {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

/* Vitals with room to be read: a name, a bulb strip and the actual figure.
   On the board these were thirty pixels of two-pixel bar and could not have
   carried a number at all. */
.duel-roster-meter-row {
  display: grid;
  grid-template-columns: 22px 1fr 18px;
  align-items: center;
  gap: var(--g-2);
}

.duel-roster-meter-label {
  font-family: var(--g-font-stencil);
  font-size: 9px;
  letter-spacing: var(--g-track-label);
  color: var(--g-ink-low);
}

.duel-roster-meter {
  position: relative;
  height: 6px;
  border-radius: 1px;
  background: var(--g-seam);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.9);
  overflow: hidden;
}

.duel-roster-meter-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  background-image: repeating-linear-gradient(
    90deg,
    var(--duel-meter) 0 4px,
    transparent 4px 6px
  );
  transition: width var(--g-move);
}

.duel-roster-meter-value {
  font-family: var(--g-font-mono);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--g-ink-mid);
}

/* -------------------------------------------------------------------------
   THE RAILS, LYING DOWN
   Under 1100px the rails become strips and each slot collapses to a portrait
   with its health under it. The names go: at this size the artwork identifies
   a piece faster than four truncated letters would.
   ------------------------------------------------------------------------- */

.duel-stage--stacked .duel-roster-rail {
  width: 100%;
  max-width: 560px;
  flex-direction: row;
  align-items: center;
  gap: var(--g-2);
  padding: var(--g-2);
}

.duel-stage--stacked .duel-roster-header {
  flex: 0 0 auto;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  padding: 0 var(--g-2) 0 0;
  border-bottom: 0;
  border-right: var(--g-hairline) solid var(--g-seam);
  background: none;
}

.duel-stage--stacked .duel-roster-entries {
  flex-direction: row;
  justify-content: center;
  overflow-x: auto;
  overflow-y: hidden;
  padding: 0;
  scrollbar-width: none;
}

.duel-stage--stacked .duel-roster-entries::-webkit-scrollbar {
  display: none;
}

.duel-stage--stacked .duel-roster-entry {
  grid-template-columns: 1fr;
  justify-items: center;
  gap: 2px;
  width: 46px;
  flex: 0 0 auto;
  padding: 3px;
  border-left: var(--g-hairline) solid var(--g-seam);
  border-top: 2px solid var(--duel-team);
}

.duel-stage--stacked .duel-roster-entry--selected { border-top-color: var(--g-lamp-amber); }
.duel-stage--stacked .duel-roster-entry--targetable { border-color: var(--g-hazard); }
.duel-stage--stacked .duel-roster-entry--unset { border-top-style: dashed; }

.duel-stage--stacked .duel-roster-name,
.duel-stage--stacked .duel-roster-meter-label,
.duel-stage--stacked .duel-roster-meter-value {
  display: none;
}

.duel-stage--stacked .duel-roster-meter-row {
  grid-template-columns: 1fr;
}

.duel-stage--stacked .duel-roster-ident {
  justify-content: center;
}

.duel-stage--stacked .duel-roster-body {
  width: 100%;
}


/* The deeper reading on a piece, docked in its own slot. This was an offcanvas
   sheet that opened centred over the board, so asking what a piece was cost you
   sight of the thing you were asking about. */
.duel-roster-entry--expanded {
  background: var(--g-hull);
}

.duel-roster-detail {
  margin-top: var(--g-2);
  padding-top: var(--g-2);
  border-top: var(--g-hairline) solid var(--g-seam);
  display: flex;
  flex-direction: column;
  gap: var(--g-3);
}

.duel-roster-detail-stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1px var(--g-3);
  margin: 0;
}

.duel-roster-stat {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--g-2);
}

.duel-roster-stat-label {
  font-family: var(--g-font-stencil);
  font-size: 9px;
  letter-spacing: var(--g-track-label);
  text-transform: uppercase;
  color: var(--g-ink-low);
  margin: 0;
}

.duel-roster-stat-value {
  font-family: var(--g-font-mono);
  font-size: var(--g-size-small);
  font-variant-numeric: tabular-nums;
  color: var(--g-ink);
  margin: 0;
}

.duel-roster-detail-matchups {
  display: flex;
  flex-wrap: wrap;
  min-width: 0;
  gap: var(--g-2);
  padding: var(--g-2);
  background: var(--g-hull-lo);
  border: var(--g-hairline) solid var(--g-seam);
  border-radius: var(--g-radius-sm);
  box-shadow: var(--g-recess);
}

.duel-roster-detail-matchups > * {
  flex: 1 1 0;
  min-width: 0;
}

.duel-roster-detail-matchups .xalian-species-badge,
.duel-roster-detail-matchups svg {
  max-width: 22px;
  max-height: 22px;
}

/* the expansion is a detail, not a mode: it never appears in the lying-down
   rails, where there is no room for it */
.duel-stage--stacked .duel-roster-detail {
  display: none;
}

/* -------------------------------------------------------------------------
   Moved from public/assets/css/pages/lobbies.css (phase 2 lobbies brief):
   duelPage.js (still v3, phase 3) reads these two layout classes. Delete
   once the duel board gets its own v4 pass. Do not add to them.
   ------------------------------------------------------------------------- */

.registry-shell {
	display: flex;
	flex-direction: column;
	gap: var(--g-5);
}

.registry-counter {
	display: flex;
	flex-direction: column;
	gap: var(--g-4);
}
