/* ═══════════════════════════════════════════════════════════════
   css/lab.css — Duel Lab (public, Pro)
   Prefix: plab-   ·   see DESIGN.md §4 class-prefix registry

   Prefix is plab-, not lab-: admin/duel-lab/lab.css already owns lab-, and
   two stylesheets claiming one prefix is how the matchup card in the admin
   lab silently inherited a rule meant for a two-letter "VS" label.
   ═══════════════════════════════════════════════════════════════ */

.plab-wrap { max-width: 1040px; margin: 0 auto; padding: 0 var(--page-px) 40px; }

.plab-head { padding: 20px 0 6px; }
.plab-title { margin: 0 0 6px; font: 800 24px/1.2 var(--font-ui); color: var(--text-1); }
.plab-sub { margin: 0; max-width: 62ch; font: 500 13px/1.65 var(--font-ui); color: var(--text-3); }
.plab-sub strong { color: var(--text-2); font-weight: 700; }

/* ─────────────────────────────────────────────────────────────
   PICK TWO — laid out like the DSB matchup header
───────────────────────────────────────────────────────────── */
.plab-vs-wrap {
  display: grid;
  grid-template-columns: 1fr 48px 1fr;
  gap: 12px;
  align-items: start;
  padding: 16px 0;
}
.plab-side { display: flex; flex-direction: column; gap: 10px; min-width: 0; }
.plab-side-label {
  font-family: var(--font-data);
  font-size: 10px;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  color: var(--text-3);
}
.plab-vs-center { display: flex; align-items: center; justify-content: center; padding-top: 28px; }
/* The VS token: a ring with a tracer running round it forever.
   The border is gone — the SVG track IS the ring now, so the stroke the tracer
   runs along and the outline of the token are one thing rather than two that
   can disagree by a pixel.
   Font stays on the token itself, not only on .plab-vs-txt: the picker at the
   top of the page uses this class with a bare "VS" text node and no SVG. */
.plab-vs-circle {
  position: relative;
  width: 34px; height: 34px;
  border-radius: 50%;
  background: var(--glass-bg-2);
  display: grid; place-items: center;
  font-family: var(--font-data);
  font-size: 9px; font-weight: 700;
  color: var(--text-3);
  box-shadow: inset 0 1px 0 var(--glass-shine);
}
.plab-vs-ring { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.plab-vs-track { fill: none; stroke: var(--border-2); stroke-width: 1.4; }
/* pathLength="100" so the dash is a readable "22% of the ring", not a number
   that has to be recomputed whenever the radius moves */
.plab-vs-arc {
  fill: none; stroke: var(--text-2); stroke-width: 2;
  stroke-linecap: round;
  stroke-dasharray: 22 78;
  transform-origin: 50% 50%;
  animation: plab-vs-orbit 2.6s linear infinite;
}
.plab-vs-txt { position: relative; }
@keyframes plab-vs-orbit { to { transform: rotate(360deg); } }

.plab-search { width: 100%; }
/* Scrolls rather than truncating: the whole complaint was names that existed
   and never appeared. 300px is roughly seven rows — enough to see there is
   more without the dropdown swallowing the page. */
.plab-results {
  display: grid; gap: 3px;
  max-height: 300px;
  overflow-y: auto;
  overscroll-behavior: contain;
}
/* Every cell placed EXPLICITLY: the avatar spans both rows, and grid
   auto-placement would otherwise lay the row out as avatar · meta · name. */
.plab-res {
  display: grid;
  grid-template-columns: 30px 1fr;
  gap: 1px 9px;
  padding: 7px 9px; text-align: left;
  background: var(--glass-bg-3);
  border: 1px solid var(--border-1);
  border-radius: 8px;
  cursor: pointer;
}
.plab-res:hover { border-color: var(--accent-border); background: var(--accent-glow-2); }
.plab-res-av {
  grid-column: 1; grid-row: 1 / span 2; align-self: center;
  width: 30px; height: 30px;
  border-radius: 50%;
  background: var(--bg-root) center/cover no-repeat;
  box-shadow: inset 0 0 0 1px var(--border-2);
}
/* Placeholder until enrichResults() lands the real photo — the initial is a
   better holding state than an empty disc, and the swap is one property. */
.plab-res-av-none {
  display: grid; place-items: center;
  font: 800 13px/1 var(--font-ui); color: var(--text-3);
}
.plab-res-name { grid-column: 2; grid-row: 1; font: 600 12px/1.3 var(--font-ui); color: var(--text-1); }
.plab-res-meta { grid-column: 2; grid-row: 2; font: 500 10px/1.4 var(--font-ui); color: var(--text-3); }
.plab-res-note { padding: 7px 2px; font: 500 11px/1.4 var(--font-ui); color: var(--text-3); }

.plab-card { display: flex; align-items: center; gap: 14px; min-width: 0; }
.plab-card-id { display: grid; gap: 2px; min-width: 0; }
.plab-card-name {
  font: 700 14px/1.3 var(--font-ui); color: var(--text-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plab-card-meta { font: 500 10.5px/1.45 var(--font-ui); color: var(--text-3); }
.plab-card-meta i { font-style: normal; opacity: .8; }
/* The canonical squad palette, from css/base.css and nowhere else.
   DESIGN.md documents this going wrong in SIX files at once (2026-08-09), each
   carrying its own copy of the OLD palette — the one where blue meant Air,
   while blue means TANK everywhere else. (The offending values are quoted in
   DESIGN.md; they are deliberately NOT repeated here, because the audit that
   guards this rule is a grep and a comment would trip it.) An earlier cut of
   this file made the same mistake a third way, by borrowing --blue/--amber and
   --accent: that put missile on the accent green when missile is gold.
   Never a page-local hex, and never resolved in JS — the light theme defines a
   different, darker set, so a frozen getComputedStyle value survives a theme
   toggle as the wrong colour. */
.plab-t-air     { color: var(--squad-air); }
.plab-t-tank    { color: var(--squad-tank); }
.plab-t-missile { color: var(--squad-missile); }

/* ─────────────────────────────────────────────────────────────
   THE COIN, AND THE RECORD THAT UNFOLDS FROM IT

   A coin, not a cube: the thing worth showing is ONE face — the player — and a
   turning cube spent three quarters of its rotation hiding it. The coin keeps
   the photo toward you and gives its edge to the squad colour.
───────────────────────────────────────────────────────────── */
.plab-badge {
  display: grid; justify-items: center;
  width: 78px;
  /* NO colour here on purpose: the squad tint lives on .plab-rec and has to
     inherit down. Setting it locally overrode the type and left the frame and
     the plate both drawn in the fallback grey. */
}
.plab-badge-in {
  position: relative;
  width: 68px; height: 68px;
  /* Float, no spin. The frame is square now, and a square turning on its Y
     axis goes edge-on the same way the coin did — the face is the point. */
  animation: plab-float 5.2s ease-in-out infinite;
}
.plab-badge-face {
  position: absolute; inset: 0;
  border-radius: 8px;
  background: var(--bg-root) center/cover no-repeat;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, .10),
    0 8px 18px -8px rgba(0, 0, 0, .8);
}
/* A drop shadow tuned for a near-black page is a bruise on a cream one. */
:root[data-theme="light"] .plab-badge-face {
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, .06),
    0 6px 14px -8px rgba(15, 23, 42, .45);
}
.plab-badge-none { display: grid; place-items: center; font: 800 26px/1 var(--font-ui); color: var(--text-3); }

/* The frame art: a single rect that draws itself around the portrait.
   overflow:visible so the stroke is not clipped in half by the viewBox edge —
   a 2.6 stroke centred on x=2 would otherwise lose its outer half. */
.plab-badge-trace {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  overflow: visible;
  pointer-events: none;
}
.plab-badge-trace rect {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.6;
  stroke-linejoin: round;
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  /* Draws ONCE and stays. A frame looping forever beside four numbers is a
     progress bar that never finishes — it pulls the eye back every cycle. */
  animation: plab-draw 1.15s ease-out .12s both;
}

/* The warzone, filled rather than written: it is the one value that says WHERE
   this fight is, so it gets the badge treatment instead of a chip. It sits
   BELOW the portrait, clear of it — overlapping the frame's bottom edge read
   as a band across the photo rather than a label under it. */
.plab-badge-wz {
  margin-top: 5px;
  padding: 2px 9px;
  border-radius: 5px;
  background: var(--tint);
  color: var(--bg-root);
  font-family: var(--font-data);
  font-size: 9px; font-weight: 700; letter-spacing: .1em;
  line-height: 1.5;
  white-space: nowrap;
  box-shadow: 0 2px 8px -3px rgba(0, 0, 0, .9);
  animation: plab-arrive .4s cubic-bezier(.2, .8, .3, 1) .9s both;
}

/* The tint is a CUSTOM PROPERTY, not just `color`.
   `background: currentColor` on the warzone plate could never work: that
   element sets its own `color` (the page background, for the text), and
   currentColor in any property other than `color` resolves against the SAME
   element's color — so the plate painted itself page-background on
   page-background. A variable is immune to that.

   Compounded with .plab-rec ON PURPOSE. The fallback lives on .plab-rec, and
   a bare .plab-badge-air is the same specificity — so whichever is written
   later wins, which is exactly how the tint lost twice: the frame and the
   warzone plate both drew in fallback grey. Two classes beats one, whatever
   the order in the file. */
.plab-rec.plab-badge-tank    { --tint: var(--squad-tank); }
.plab-rec.plab-badge-air     { --tint: var(--squad-air); }
.plab-rec.plab-badge-missile { --tint: var(--squad-missile); }

@keyframes plab-draw  { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } }
@keyframes plab-float {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-6px); }
}

/* ── the record, radiating from the coin ───────────────────────
   Two chips out of each side, each on its own line back to the coin. Three
   columns with the coin spanning both rows keeps the lines short and the coin
   genuinely central — a flex row would centre the BOX, not the disc. */
.plab-rec {
  --tint: var(--border-2);           /* until .plab-badge-<type> sets one */
  color: var(--tint);                /* so stroke/line currentColor follows */
  display: grid;
  /* minmax, not 1fr: plain 1fr is minmax(auto, 1fr), and .plab-chip carries
     min-width:0, which removes the automatic min-content floor — the column
     then squeezed "259.73M" down to "259.7…" on a 900px screen with room to
     spare. 84px is the widest value the chips carry at this size. */
  grid-template-columns: minmax(84px, 1fr) auto minmax(84px, 1fr);
  grid-template-rows: auto auto;
  align-items: center;
  gap: 6px 8px;
  width: 100%;
}
.plab-badge { grid-column: 2; grid-row: 1 / span 2; }
.plab-chip-tl { grid-column: 1; grid-row: 1; justify-self: end; }
.plab-chip-bl { grid-column: 1; grid-row: 2; justify-self: end; }
.plab-chip-tr { grid-column: 3; grid-row: 1; justify-self: start; }
.plab-chip-br { grid-column: 3; grid-row: 2; justify-self: start; }

.plab-chip {
  display: flex; align-items: center; gap: 6px;
  min-width: 0; max-width: 100%;
  /* Arrive one behind the next, then STAY. `both` holds the from-state during
     the delay so nothing flashes before its turn, and the to-state after — a
     looping reveal would redraw itself under someone reading a number. */
  animation: plab-arrive .42s cubic-bezier(.2, .8, .3, 1) both;
  animation-delay: calc(.34s + var(--i) * .12s);
}
/* left-hand chips read outward-in, so their line sits on the right */
.plab-chip-tl, .plab-chip-bl { flex-direction: row; }
.plab-chip-tl .plab-chip-body, .plab-chip-bl .plab-chip-body { order: -1; text-align: right; }
.plab-chip-tl .plab-chip-glyph, .plab-chip-bl .plab-chip-glyph { order: -2; }

/* the line back to the coin — grows out of it, then holds */
.plab-chip-line {
  flex: none;
  width: 18px; height: 1px;
  background: linear-gradient(90deg, var(--border-1), currentColor);
  opacity: .6;
  animation: plab-reach .3s ease-out both;
  animation-delay: calc(.28s + var(--i) * .12s);
}
.plab-chip-tl .plab-chip-line, .plab-chip-bl .plab-chip-line {
  background: linear-gradient(270deg, var(--border-1), currentColor);
  transform-origin: 100% 50%;
}
.plab-chip-tr .plab-chip-line, .plab-chip-br .plab-chip-line { transform-origin: 0 50%; }

.plab-chip-body { display: grid; gap: 0; min-width: 0; }
.plab-chip-k {
  font-family: var(--font-data); font-size: 8px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-3);
  white-space: nowrap;
}
.plab-chip-v {
  font: 700 11.5px/1.3 var(--font-ui); color: var(--text-1);
  font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plab-chip-v small { display: block; font: 500 9px/1.3 var(--font-ui); color: var(--text-3); }
.plab-chip-v i { font-style: normal; color: var(--text-3); font-weight: 500; }
.plab-chip-glyph { position: relative; display: block; width: 15px; height: 15px; flex: none; }

/* The jet flies past instead of sitting still — it is the one glyph that reads
   as being in motion, so holding it static looks parked. Runs once with the
   arrival, then settles: a plane looping across a data chip forever would pull
   the eye off the number it belongs to. */
.plab-flyby .plab-glyph {
  animation: plab-flyby 1.05s cubic-bezier(.25, .7, .25, 1) both;
  animation-delay: calc(.5s + var(--i) * .12s);
}
@keyframes plab-flyby {
  0%   { transform: translateX(-16px) rotate(-16deg); opacity: 0; }
  55%  { opacity: 1; }
  100% { transform: none; opacity: 1; }
}

@keyframes plab-reach  { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes plab-arrive {
  from { opacity: 0; transform: translateY(-8px) scale(.94); }
  to   { opacity: 1; transform: none; }
}

/* ── the squad glyph, filled + traced ──────────────────────────── */
.plab-glyph { display: block; width: 100%; height: 100%; fill: currentColor; }
.plab-glyph-trace {
  position: absolute; inset: 0;
  fill: none; stroke: currentColor; stroke-width: 1.1;
  stroke-linecap: round; stroke-linejoin: round;
}
.plab-glyph-trace .plab-trace {
  stroke-dasharray: 100;
  stroke-dashoffset: 100;
  animation: plab-trace 3.4s ease-in-out infinite;
}
/* each stroke a beat behind the last, so a multi-part glyph lays itself down
   rather than every stroke appearing at once */
.plab-glyph-trace .plab-trace:nth-child(2) { animation-delay: .16s; }
.plab-glyph-trace .plab-trace:nth-child(3) { animation-delay: .32s; }
@keyframes plab-trace {
  0%   { stroke-dashoffset: 100; }
  45%  { stroke-dashoffset: 0; }
  80%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -100; }
}

/* Motion off: everything is already where it belongs, it just never travelled.
   The reveal is decoration; the record is the content. */
@media (prefers-reduced-motion: reduce) {
  .plab-badge-in { animation: none; transform: none; }
  .plab-badge-trace rect { animation: none; stroke-dashoffset: 0; }
  .plab-badge-wz { animation: none; opacity: 1; transform: none; }
  /* the ring stays, it just stops going round */
  .plab-vs-arc { animation: none; }
  .plab-chip { animation: none; opacity: 1; transform: none; }
  .plab-chip-line { animation: none; transform: none; }
  .plab-flyby .plab-glyph { animation: none; transform: none; opacity: 1; }
  .plab-glyph-trace .plab-trace { animation: none; stroke-dashoffset: 0; }
}

/* ─────────────────────────────────────────────────────────────
   PANELS
───────────────────────────────────────────────────────────── */
.plab-panel {
  margin-top: 14px;
  padding: 15px 16px;
  background: var(--bg-1);
  border: 1px solid var(--border-1);
  border-radius: 12px;
}
.plab-h { margin: 0 0 10px; font: 700 13px/1.3 var(--font-ui); color: var(--text-1); }
.plab-note { margin: 10px 0 0; font: 500 11.5px/1.6 var(--font-ui); color: var(--text-3); }
.plab-note b { color: var(--text-2); }
.plab-note-lead { margin: 0 0 14px; }
.plab-fineprint { opacity: .85; }

/* The squad-power prediction, shown BESIDE the empty box rather than inside it.
   Deliberately quiet and uncoloured: the gap between our estimate and the
   player's reading is the measurement this page is collecting, not an error to
   nudge them away from. Anything that made the number look "wrong" would push
   players toward typing ours back at us. */
.plab-pred {
  margin: 8px 0 0;
  font: 500 11px/1.6 var(--font-ui);
  color: var(--text-3);
}
.plab-pred b { color: var(--text-2); font-family: var(--font-data); }
.plab-pred-gap { color: var(--text-2); }

/* ─────────────────────────────────────────────────────────────
   FIGHT ARENA — two fighters facing off.

   SIDE BY SIDE AT EVERY WIDTH, including phones. Stacking them would turn a
   face-off into a list, and the whole point is reading the two against each
   other; the columns shrink instead.
───────────────────────────────────────────────────────────── */
/* 3D-STYLED, NOT WEBGL — the same call dsb.css makes for its battle forecast:
   a GPU context to draw a receding floor costs ~600KB for something
   perspective + rotate does for free. Transforms are compositor-only, there is
   no per-frame JS, and it degrades to a flat panel wherever preserve-3d is
   missing. `align-items: end` is what stands the fighters ON the floor rather
   than floating them above it. */
.plab-arena {
  position: relative;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 10px;
  align-items: end;
  padding: 22px 0 16px;
  min-height: 230px;
  perspective: 900px;
  perspective-origin: 50% 120%;
  border-radius: 12px;
  overflow: hidden;
}
.plab-stage { position: absolute; inset: 0; pointer-events: none; }
/* The globe canvas: under the fighters, over the CSS floor. pointer-events
   none so it never eats a tap meant for the panel, aria-hidden by virtue of
   sitting inside .plab-stage — the numbers in front carry the real meaning and
   the globe is the same figure drawn as area. */
.plab-gl {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  display: block;
  z-index: 0;
  pointer-events: none;
}
/* The fighters and the VS must sit ABOVE the canvas, which is painted last in
   source order inside the stage. */
.plab-fighter, .plab-arena-mid { position: relative; z-index: 1; }
/* Receding floor: one element, two repeating gradients, no DOM per line. */
.plab-floor {
  position: absolute; inset: auto 0 0 0; height: 76px;
  transform: rotateX(64deg); transform-origin: 50% 100%;
  background:
    repeating-linear-gradient(90deg, transparent 0 38px, rgba(148, 163, 184, .10) 38px 39px),
    repeating-linear-gradient(0deg,  transparent 0 26px, rgba(148, 163, 184, .08) 26px 27px);
  -webkit-mask-image: linear-gradient(0deg, #000, transparent 88%);
          mask-image: linear-gradient(0deg, #000, transparent 88%);
}
/* Each half of the floor takes its fighter's WIN colour — the side that is
   ahead burns accent, the side behind burns red — so the ground under the
   fight says who is winning before either number is read. Squad colour was
   the earlier choice and it was the wrong signal here: the type is already on
   the frame, the plate and the glyph, and the floor is the one surface big
   enough to carry the outcome.
   Opacity is set inline from glowFor(), scaled by distance from even. */
.plab-floor-half {
  position: absolute; bottom: 0; width: 52%; height: 92px;
  color: transparent;
  background: radial-gradient(65% 85% at 50% 100%, currentColor, transparent 74%);
  -webkit-mask-image: linear-gradient(0deg, #000, transparent 92%);
          mask-image: linear-gradient(0deg, #000, transparent 92%);
}
.plab-floor-a { left: 0; }
.plab-floor-b { right: 0; }
.plab-floor-half.win  { color: var(--accent); }
.plab-floor-half.lose { color: var(--red); }
/* Light theme: the same glow that reads as light spilling onto a dark floor
   reads as a stain on a pale one. The opacity is set inline by glowFor(), so
   this scales it back with a filter rather than fighting the attribute. */
:root[data-theme="light"] .plab-floor-half { filter: saturate(.85) opacity(.5); }
:root[data-theme="light"] .plab-floor {
  background:
    repeating-linear-gradient(90deg, transparent 0 38px, rgba(71, 85, 105, .10) 38px 39px),
    repeating-linear-gradient(0deg,  transparent 0 26px, rgba(71, 85, 105, .08) 26px 27px);
}
.plab-fighter {
  display: grid;
  justify-items: center;
  gap: 6px;
  min-width: 0;
  padding: 12px 8px;
  border: 1px solid transparent;
  border-radius: 12px;
}
/* No box on the leader. Who is ahead is already said three times over — the
   percentage is accent, the floor under them burns accent, and the troop line
   tells you who was wiped. A fourth marker was the one that boxed in half the
   arena and fought the globe behind it. */
.plab-fname {
  max-width: 100%;
  font: 700 13px/1.3 var(--font-ui); color: var(--text-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plab-fpct { font: 800 22px/1.05 var(--font-ui); color: var(--text-1); font-variant-numeric: tabular-nums; }
.plab-fighter.lead .plab-fpct { color: var(--accent); }
.plab-ftroops {
  font: 700 11.5px/1.3 var(--font-ui); color: var(--text-2);
  font-variant-numeric: tabular-nums; text-align: center;
}
.plab-ftroops small { font-weight: 500; color: var(--text-3); }
.plab-ftroops.wiped { color: var(--red); }

/* Level with the AVATARS, not the middle of the whole fighter column: the
   fighters run on below their portraits into name, percentage and troop line,
   so centring on the column dropped the token to the percentages. align-self
   start plus the portrait's own offset puts it on their eye-line. */
.plab-arena-mid { display: grid; justify-items: center; align-self: start; padding-top: 33px; }


@keyframes plab-trace {
  0%   { stroke-dashoffset: 100; }
  45%  { stroke-dashoffset: 0; }
  80%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -100; }
}

/* ─────────────────────────────────────────────────────────────
   THE REPORT DESK

   Built to sit under the arena rather than beside it: same card language, same
   squad tint on the portrait, so reporting a fight reads as part of the fight
   and not as a form bolted underneath one.
───────────────────────────────────────────────────────────── */
.plab-form { display: grid; gap: 14px; }
.plab-desk { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

.plab-deskside {
  --tint: var(--border-2);
  display: grid; gap: 12px;
  min-width: 0;
  padding: 13px 12px;
  background: var(--glass-bg-3);
  border: 1px solid var(--border-1);
  border-radius: 12px;
}
.plab-deskside.plab-badge-tank    { --tint: var(--squad-tank); }
.plab-deskside.plab-badge-air     { --tint: var(--squad-air); }
.plab-deskside.plab-badge-missile { --tint: var(--squad-missile); }

.plab-who { display: flex; align-items: center; gap: 10px; min-width: 0; }
.plab-who-av {
  width: 38px; height: 38px; flex: none;
  border-radius: 9px;
  background: var(--bg-root) center/cover no-repeat;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .08), 0 0 0 2px var(--tint);
}
.plab-who-none { display: grid; place-items: center; font: 800 16px/1 var(--font-ui); color: var(--text-3); }
.plab-who-id { display: grid; gap: 1px; min-width: 0; }
.plab-who-name {
  font: 700 13px/1.3 var(--font-ui); color: var(--text-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plab-who-meta { font: 500 10px/1.35 var(--font-ui); color: var(--text-3); }

/* three faders a side, laid out like a mixing desk */
.plab-faders { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
.plab-fader {
  display: grid; justify-items: center; gap: 6px;
  padding: 9px 5px;
  background: var(--bg-1);
  border: 1px solid var(--border-1);
  border-radius: 9px;
}
.plab-fader-k {
  font-family: var(--font-data); font-size: 8px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3); text-align: center;
  white-space: nowrap;
}
.plab-fader-k i { font-style: normal; opacity: .7; }
/* Vertical range: the appearance keywords are the only way to get one, and
   they differ per engine — writing-mode covers Firefox, slider-vertical the
   WebKit/Blink pair. Same trio the admin lab's faders use. */
.plab-fader-range {
  -webkit-appearance: slider-vertical;
  appearance: slider-vertical;
  writing-mode: vertical-lr;
  direction: rtl;
  width: 24px; height: 96px;
  padding: 0;
  accent-color: var(--tint);
  cursor: pointer;
}
.plab-fader-num {
  width: 100%;
  padding: 4px 2px;
  text-align: center;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}

.plab-meta { display: grid; grid-template-columns: repeat(2, 1fr) 2fr; gap: 8px; }
.plab-fmeta { display: grid; gap: 3px; min-width: 0; }
.plab-fmeta > span {
  font-family: var(--font-data); font-size: 9px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3);
}

/* ── reading the report ────────────────────────────────────────
   The calculator sits inside each fighter's card, collapsed: a player who
   already knows the sum should not have to scroll past four boxes to reach the
   faders, and one who does not should not have to be told twice. */
.plab-calc { margin-top: 2px; }
.plab-calc-h {
  cursor: pointer; list-style: none;
  font-family: var(--font-data); font-size: 9px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-3);
  padding: 5px 0;
}
.plab-calc-h::-webkit-details-marker { display: none; }
.plab-calc-h::before { content: '+ '; opacity: .7; }
.plab-calc[open] .plab-calc-h::before { content: '– '; }
.plab-calc-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
.plab-calc-f { display: grid; gap: 2px; min-width: 0; }
.plab-calc-f > span {
  font-family: var(--font-data); font-size: 7.5px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--text-3);
}
.plab-calc-in { padding: 4px 2px; text-align: center; font-size: 10.5px; font-variant-numeric: tabular-nums; }
.plab-calc-out {
  margin-top: 7px;
  font: 500 10.5px/1.45 var(--font-ui); color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.plab-calc-out.ok b { color: var(--text-1); font-weight: 700; }
.plab-calc-warn { color: var(--amber); }
.plab-calc-note { color: var(--text-3); opacity: .85; }

/* the two reference shots, side by side and scaled to the column */
.plab-guide { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.plab-guide-f { margin: 0; display: grid; gap: 5px; }
.plab-guide-f img {
  width: 100%; height: auto; display: block;
  border: 1px solid var(--border-1); border-radius: 9px;
  background: var(--glass-bg-3);
}
.plab-guide-f figcaption { font: 500 10.5px/1.45 var(--font-ui); color: var(--text-3); }
.plab-guide-f figcaption b { color: var(--text-2); }

/* the screenshot field — required, and it says so before it is asked for */
.plab-shot {
  display: grid; gap: 6px;
  padding: 11px 12px;
  background: var(--glass-bg-3);
  border: 1px dashed var(--border-2);
  border-radius: 10px;
}
.plab-shot-k {
  font-family: var(--font-data); font-size: 9px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3);
}
.plab-shot-k b { color: var(--amber); font-weight: 700; }
.plab-shot-input { font: 500 11px/1.4 var(--font-ui); color: var(--text-2); }
.plab-shot-state { font: 600 11px/1.4 var(--font-ui); color: var(--text-3); min-height: 16px; }
.plab-shot-state.ok  { color: var(--accent); }
.plab-shot-state.bad { color: var(--red); }
.plab-shot-req { margin-top: 0; }
.plab-shot-req b { color: var(--text-2); }
/* a disabled submit must look disabled, not merely refuse */
#btnSubmit:disabled { opacity: .45; cursor: not-allowed; }

.plab-quota { font: 500 11px/1.5 var(--font-ui); color: var(--text-3); margin-left: auto; }
.plab-quota.full { color: var(--amber); }

.plab-compare { display: grid; gap: 4px; margin-top: 10px; min-height: 40px; }
.plab-cmp-row { font: 500 11.5px/1.5 var(--font-ui); color: var(--text-3); }
.plab-cmp-row b { color: var(--text-2); }
.plab-cmp.ok { color: var(--accent); font-weight: 700; }
.plab-cmp.off { color: var(--amber); font-weight: 700; }
.plab-cmp-idle { font: 500 11.5px/1.5 var(--font-ui); color: var(--text-3); opacity: .8; }

.plab-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px; }
.plab-status { font: 500 11.5px/1.5 var(--font-ui); color: var(--text-3); }
.plab-status.ok { color: var(--accent); }
.plab-status.bad { color: var(--red); }

.plab-foot { margin: 26px 0 0; font: 500 10.5px/1.6 var(--font-ui); color: var(--text-3); opacity: .8; }

/* ─────────────────────────────────────────────────────────────
   PHONE — the VS token stops being a column and becomes a rule
   between the two sides, which is the only way both names keep
   their width.
───────────────────────────────────────────────────────────── */
/* 1000px, not 700. The two-sided radial needs ~290px per fighter (badge 78 +
   two chip columns + gaps) and the DESKTOP SIDEBAR eats ~250px of viewport, so
   a 900px window leaves each fighter 245 — the columns sat pinned at their
   84px floor and the record overflowed its own card by 17px, clipping
   "ALLIANCE" to "ALLIAN". Measured, not guessed:
     viewport 900 → fighter 245 → rec 262   overflow
   Below this width the one-sided fan is the layout; above it there is genuinely
   room for chips on both sides. */
@media (max-width: 1000px) {
  .plab-vs-wrap { grid-template-columns: 1fr; gap: 14px; }
  .plab-vs-center { padding: 0; }
  .plab-vs-circle { width: 28px; height: 28px; font-size: 8px; }
  /* the arena does NOT stack — see its rule above */
  .plab-arena { gap: 4px; }
  .plab-fighter { padding: 10px 2px; gap: 5px; }
  /* PHONE: still radiating, still linked — but out of ONE side of the coin.
     The arithmetic decides this, not taste. At 360px the fighter column is
     ~120px; chips on both sides leaves each about 20px, which fits nothing.
     Chips on one side leaves ~53px, and "475.48M" needs 46. So each fighter
     keeps its lines and its radiating shape, and the two coins face each other
     across the VS with their records fanning outward.
     (A 2×2 block under the coin was the earlier answer here. It kept the
     numbers and lost the thing that made them read as one record.) */
  .plab-rec {
    grid-template-columns: minmax(52px, 1fr) auto;
    grid-template-rows: repeat(4, auto);
    gap: 2px 4px;
    align-items: center;
  }
  .plab-badge { grid-column: 2; grid-row: 1 / span 4; }
  .plab-chip-tl { grid-column: 1; grid-row: 1; justify-self: end; }
  .plab-chip-tr { grid-column: 1; grid-row: 2; justify-self: end; }
  .plab-chip-bl { grid-column: 1; grid-row: 3; justify-self: end; }
  .plab-chip-br { grid-column: 1; grid-row: 4; justify-self: end; }
  /* every chip now sits LEFT of the coin, so every one reads outward-in */
  .plab-chip { flex-direction: row; }
  .plab-chip-body { order: -1; text-align: right; }
  .plab-chip-glyph { order: -2; }
  .plab-chip-line {
    background: linear-gradient(270deg, var(--border-1), currentColor);
    transform-origin: 100% 50%;
  }
  /* Side B mirrors, so both records fan AWAY from the VS in the middle
     rather than both pointing the same way. */
  .plab-fighter-b .plab-rec { grid-template-columns: auto minmax(52px, 1fr); }
  .plab-fighter-b .plab-badge { grid-column: 1; }
  .plab-fighter-b .plab-chip-tl,
  .plab-fighter-b .plab-chip-tr,
  .plab-fighter-b .plab-chip-bl,
  .plab-fighter-b .plab-chip-br { grid-column: 2; justify-self: start; }
  .plab-fighter-b .plab-chip { flex-direction: row; }
  .plab-fighter-b .plab-chip-body { order: 0; text-align: left; }
  .plab-fighter-b .plab-chip-glyph { order: 1; }
  .plab-fighter-b .plab-chip-line {
    background: linear-gradient(90deg, var(--border-1), currentColor);
    transform-origin: 0 50%;
  }
  .plab-badge { width: 50px; }
  .plab-badge-in { width: 46px; height: 46px; }
  .plab-badge-wz { font-size: 7.5px; padding: 1px 6px; letter-spacing: .06em; }
  .plab-chip { gap: 3px; }
  .plab-chip-line { width: 8px; }
  .plab-chip-k { font-size: 6.5px; letter-spacing: .04em; }
  .plab-chip-v { font-size: 9.5px; }
  .plab-chip-v small { font-size: 7.5px; }
  .plab-chip-glyph { width: 12px; height: 12px; }
  .plab-desk { grid-template-columns: 1fr; }
  .plab-guide { grid-template-columns: 1fr; }
  .plab-calc-grid { grid-template-columns: repeat(2, 1fr); }
  .plab-meta { grid-template-columns: 1fr 1fr; }
  .plab-fmeta-wide { grid-column: 1 / -1; }
  .plab-fader-range { height: 84px; }
  .plab-fpct { font-size: 18px; }
  .plab-fname { font-size: 11.5px; }
    .plab-arena-mid { padding-top: 34px; }
  .plab-frow { grid-template-columns: 1fr 1fr; }
  .plab-frow-head { display: none; }
  .plab-fname { grid-column: 1 / -1; }
  .plab-frow-meta { grid-template-columns: 1fr 1fr; }
  .plab-fmeta-wide { grid-column: 1 / -1; }
}
