/* HR Portal — High Rollers branding, on glass.
 *
 * The swatches are the authentic brand hexes, lifted verbatim from the daemon
 * dashboard's :root so the two surfaces cannot drift. They come from the logo:
 * casino felt, chip gold, card cream, ink outlines.
 *
 * Where this departs from the dashboard: the dashboard paints TAN panels and
 * puts INK text on them, which is right for a dense operator console. Here the
 * panels are dark translucent glass over the felt, with CREAM text. Same
 * palette, more depth — the felt and its glows show through, panels lift off
 * the background instead of sitting flat on it, and the gold reads as an accent
 * rather than as a fifth surface colour.
 *
 * Contrast was the constraint, not the look. Cream #FBE0A4 on the glass over
 * felt-deep is comfortably past 4.5:1; the muted tone is Tan, not a dimmed
 * cream, so secondary text stays legible instead of turning to mud. The
 * dashboard's dark --ok/--warn/--bad derivatives exist for text on TAN and are
 * deliberately NOT used here — on dark glass the bright brand hues are the
 * readable ones, which is the same reasoning applied to the opposite surface.
 */
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');

:root {
  /* brand swatches (authentic hex) */
  --ink:#190502; --felt:#288330; --felt-deep:#1D5626; --gold:#CE7806;
  --gold-bright:#FFD527; --cream:#FBE0A4; --card-white:#E9E9EA;
  --chip-peach:#F1AC74; --tan:#B79672; --white:#FFFFFF; --red:#c23c2e;
  /* Not a brand colour and deliberately outside the palette: it is the one
     thing on this page that is neither good nor bad, and every warm hue here
     already means something. Same blue the status page uses for maintenance,
     so the two surfaces say the same thing in the same colour. */
  --info:#4c8dff;

  /* glass */
  --glass: rgba(14, 32, 16, 0.55);
  --glass-hi: rgba(255, 255, 255, 0.07);      /* top-edge highlight */
  --glass-line: rgba(251, 224, 164, 0.16);    /* cream at low alpha */
  --glass-line-hot: rgba(255, 213, 39, 0.55);
  --shadow-1: 0 1px 2px rgba(0,0,0,.28), 0 6px 16px rgba(0,0,0,.30);
  --shadow-2: 0 2px 6px rgba(0,0,0,.32), 0 18px 40px rgba(0,0,0,.42);

  /* The print on the back of a playing card, in three layers, and it exists so
     the glass has something to be glass ABOUT. Every panel on these pages
     carries backdrop-filter: blur(14px), and a backdrop that is a smooth
     gradient blurs to the same smooth gradient — the effect was doing real work
     and none of it was visible. A texture is what makes the blur legible.

     Tan (#B79672) at single-digit alpha, spelled out rather than var(--tan)
     because these need an alpha channel and a hex custom property cannot supply
     one. Two opposed diagonals at a 34px period give the woven lattice; the
     suit pips are the print on top of it. Everything is a gradient or an inline
     SVG, so the whole texture costs zero requests and no bytes beyond this file,
     and it tiles to any viewport instead of stretching.

     Alpha is the entire design here. At .16 the pips read as wallpaper and
     compete with the cards; at .095 they are a whisper unblurred and a soft
     tonal drift once a panel blurs them, which is the point. If these ever need
     turning up, turn up the WEAVE — it is directional, so it survives the blur
     better than the pips, which is why it carries the heavier alpha. */
  --weave: repeating-linear-gradient(135deg,
      rgba(183,150,114,.085) 0 3px, rgba(183,150,114,0) 3px 11px,
      rgba(183,150,114,.045) 11px 13px, rgba(183,150,114,0) 13px 34px);
  --weave-back: repeating-linear-gradient(45deg,
      rgba(183,150,114,.055) 0 3px, rgba(183,150,114,0) 3px 11px,
      rgba(183,150,114,.03) 11px 13px, rgba(183,150,114,0) 13px 34px);
  --pips: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='260' height='260' viewBox='0 0 260 260'><g fill='rgb(251,224,164)' fill-opacity='0.095'><g transform='translate(22,16) scale(2.1)'><g transform='rotate(180 12 12)'><path d='M12 21c-1-1.2-8-5.4-8-10.2A4.8 4.8 0 0 1 12 7.4 4.8 4.8 0 0 1 20 10.8C20 15.6 13 19.8 12 21Z'/></g><path d='M12 12c0 4.6-1 6-2.8 7.6h5.6C13 18 12 16.6 12 12Z'/></g><g transform='translate(152,68) scale(2.1)'><path d='M12 21c-1-1.2-8-5.4-8-10.2A4.8 4.8 0 0 1 12 7.4 4.8 4.8 0 0 1 20 10.8C20 15.6 13 19.8 12 21Z'/></g><g transform='translate(48,152) scale(2.1)'><circle cx='12' cy='7.2' r='4.3'/><circle cx='7.1' cy='13.8' r='4.3'/><circle cx='16.9' cy='13.8' r='4.3'/><path d='M12 12c0 4.6-1 6-2.8 7.6h5.6C13 18 12 16.6 12 12Z'/></g><g transform='translate(178,198) scale(2.1)'><path d='M12 2.5 19 12 12 21.5 5 12Z'/></g></g></svg>");

  /* A broad diagonal sheen, and it is the layer that makes the glass legible as
     glass. The weave and the pips above do not survive a panel, and that is
     arithmetic rather than taste: CSS `blur(14px)` is a Gaussian of standard
     deviation 14, which attenuates a sinusoid of period p by
     exp(-2*pi^2*sigma^2/p^2). At the weave's 34px period that leaves 3.5% of it,
     and 3.5% of a texture already down at single-digit alpha is nothing a person
     can see. Turning the blur down does not rescue it either — at 9px the same
     sum gives 25% of an amount too small to matter, which is why measuring the
     surviving detail behind a panel at blur 14, 11 and 9 returned 28.1%, 28.2%
     and 28.4%: three readings of the noise floor, not three blurs.

     Structure at 220px is the other end of that curve — 86% of it survives — so
     what shows through a panel is this, softened, and what shows beside the panel
     is this plus the weave, sharp. That difference across the card's edge is the
     whole effect. Measured at 46% retained through a panel, which is the glass's
     own 55% opacity and essentially nothing else.

     Last in the texture group so the fine print reads on top of it. */
  --sheen: repeating-linear-gradient(135deg,
      rgba(183,150,114,0) 0, rgba(183,150,114,.09) 110px, rgba(183,150,114,0) 220px);

  /* The felt itself, named once so the fixed layer and the sign-in splash
     cannot drift apart — they were two verbatim copies of the same four
     gradients, and this change would have had to be made twice. */
  --felt-stack:
    radial-gradient(1200px 620px at 6% -8%, rgba(40,131,48,.55), transparent 60%),
    radial-gradient(820px 420px at 100% -4%, rgba(206,120,6,.20), transparent 58%),
    radial-gradient(1100px 900px at 50% 120%, rgba(0,0,0,.55), transparent 70%),
    var(--pips), var(--weave), var(--weave-back), var(--sheen),
    linear-gradient(180deg, #1D5626 0%, #143f1c 100%);
  /* Eight layers, and the two shorthand lists below must stay eight long. The
     three glows and the base are single paints that must NOT tile — a repeating
     radial gradient is a grid of spotlights — so this cannot go back to the one
     -word `background-repeat: no-repeat` it replaced. */
  --felt-stack-size: auto, auto, auto, 280px 280px, auto, auto, auto, auto;
  --felt-stack-repeat: no-repeat, no-repeat, no-repeat, repeat, repeat, repeat, repeat, no-repeat;

  --text: var(--cream);
  --muted: var(--tan);
  --accent: var(--gold-bright);

  --font-display:'Bebas Neue',Impact,'Oswald',sans-serif;
  --font-sans:'IBM Plex Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  --font-mono:'IBM Plex Mono',ui-monospace,'Cascadia Mono',Consolas,monospace;

  --r: 14px;
}

* { box-sizing: border-box; }

/* The `hidden` attribute has to actually hide.
 *
 * The browser's own rule for it is `[hidden] { display: none }` in the UA
 * stylesheet, and ANY author rule that sets `display` beats it - a class like
 * `.btn` (inline-block) or `.row` (flex) does, and an inline `style="display:
 * flex"` does several times over. So an element written as
 * `<button class="btn" hidden>` is simply not hidden, and the JavaScript that
 * sets `.hidden = true` on it does nothing at all, silently and forever.
 *
 * Found on `vod.html`, where a "Stop watching" button, a nudge slider for a
 * VOD that did not exist and an opponents-level control for a track nobody had
 * selected were all on the page permanently. `#stats` on the matches page had
 * the same shape - an empty strip visible until the numbers arrived.
 *
 * `!important` is the whole point rather than a shortcut: the rule has to win
 * against an inline style, and nothing else can. */
[hidden] { display: none !important; }

body {
  margin: 0; padding: 0 20px 72px;
  color: var(--text);
  font: 15px/1.6 var(--font-sans);
  background-color: #12401b;
  min-height: 100vh;
}
/* Felt with table lighting: a warm pool top-left, gold spill top-right, and a
   vignette so the panels have something to float above.

   In its own fixed layer rather than as `background-attachment: fixed` on the
   body, and that is a mobile fix rather than a tidy-up. A fixed background
   attachment asks the compositor to hold a paint still against a scrolling
   element, which phone engines do on a delay or not at all — so a hard fling
   outran the vignette and you could watch the lighting catch up after the
   content had stopped. A fixed-position layer is never scrolled in the first
   place, so there is nothing for a fling to outpace.

   Sized to the LARGE viewport, and `inset: 0` was not enough — that was the
   second half of this same bug, reported after the first fix. A fixed element is
   sized to the *layout* viewport, which is the short one, measured with the
   browser's toolbars on screen. When a phone retracts its toolbar on scroll the
   visible area grows past that, and the strip that appears at the bottom is
   outside this layer: bare background-color with none of the vignette over it, as
   a hard horizontal seam across a scrolled page. `lvh` is by definition the
   viewport with the toolbars retracted, so no scroll state is taller than this.
   The `vh` line before it is the fallback for engines without `lvh`, which get
   the old behaviour rather than none. */
body::before {
  content: ""; position: fixed; top: 0; left: 0; right: 0;
  height: 100vh;
  height: 100lvh;
  z-index: -1; pointer-events: none;
  background-color: #12401b;
  background-image: var(--felt-stack);
  background-size: var(--felt-stack-size);
  background-repeat: var(--felt-stack-repeat);
}
body.shell { display: grid; place-items: center; padding: 24px; }

a { color: var(--gold-bright); text-decoration-color: rgba(255,213,39,.35); }
a:hover { color: var(--chip-peach); }
code, .mono { font-family: var(--font-mono); font-size: .88em; }

h1,h2,h3 { font-family: var(--font-display); font-weight: 400; letter-spacing: .035em; text-transform: uppercase; line-height: 1.05; }
h1 { font-size: 30px; margin: 0; text-shadow: 0 0 22px rgba(255,213,39,.3); }
h2 { font-size: 20px; margin: 30px 0 12px; }
h3 { font-size: 16px; margin: 0 0 6px; }
.eyebrow { font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--gold-bright); margin: 0 0 6px; }
/* The match page opens on an eyebrow rather than an h2, and h2's 30px top margin
   is what clears the sticky header's drop shadow on every other page — that
   shadow is offset 18px down and blurred 40px, so a zero-margin first line sits
   inside it. 14px is .warnbox's own margin, so the page starts level whether or
   not a banner is showing. Scoped to #crumb: .eyebrow is also the request
   sheet's label, mid-page, where nothing needs moving. */
#crumb { margin-top: 14px; }
.lede { color: var(--cream); opacity: .92; }
.muted { color: var(--muted); }
/* No text-transform, ever: the value is lowercase and every join against it is
   exact-match. user-select: all so one click takes the whole string — a 16-hex
   run is exactly the kind of thing a double-click selects half of. */
.checksum {
  font-family: var(--font-mono); font-size: 12px; color: var(--muted);
  letter-spacing: .04em; margin: 2px 0 0; user-select: all; word-break: break-all;
}

/* ---- the glass primitive ---- */
/* The two compositor hints on the end of this rule are a MITIGATION FOR AN
   UNREPRODUCED BUG, and saying so is the point of this comment. Do not read
   them as a confirmed fix, and do not remove them casually either.

   Reported twice: on loading the match page, a large flat untextured green
   rectangle covers the top of the round-detail card - the heading, the request
   button, the kills list - for a noticeable time before the content appears.
   The green is the felt's own base colour with none of its texture, while the
   rest of the page is textured normally. First assumed to happen only on
   resize; it does not.

   What was ruled out, by reading the code rather than by guessing: the sign-in
   splash layer (only ever appended on `?welcome=1`, and inset:0 full-viewport,
   so it cannot cover part of one card) and the lazy-thumbnail placeholder's
   #0d1f10 (near-black, not this green, and there is none anywhere inside
   `#detail`). That leaves this rule as the only thing that can paint
   over that region before the card's own content exists - `#detail` is a
   `.card` whose innerHTML is swapped from a one-line placeholder to a ~350px
   block, and a stale composited layer of the pre-swap card is exactly a flat
   blur of the felt with no content over it.

   What could NOT be established: a reproduction. Headless Chromium forces the
   compositor to a settled frame before it captures, at every virtual-time
   budget from 50ms to 5s, with and without GPU - so a sub-frame compositing
   race is invisible to it and no screenshot can confirm or refute this.

   The two properties were chosen because they measured at ZERO visual cost
   (byte-identical renders against the current build), unlike the obvious
   alternative of putting an opaque background-color under the gradient, which
   measurably masks the felt and turns the panel into a tinted card rather than
   glass - defeating the entire reason the texture above exists. `isolation`
   gives each panel its own stacking context and `will-change` asks for it to be
   promoted up front, so a content-height change should not force a blocking
   re-rasterisation of the blurred backdrop.

   If the green box is still reported after this ships, these two lines are not
   the answer and should be removed rather than added to. */
.glass, .card, .match, .sugg, .vid, dialog.sheet, .filters, .rail-item, .stat {
  background: linear-gradient(180deg, var(--glass-hi), transparent 42%), var(--glass);
  -webkit-backdrop-filter: blur(12px) saturate(120%);
  backdrop-filter: blur(12px) saturate(120%);
  border: 1px solid var(--glass-line);
  border-radius: var(--r);
  box-shadow: var(--shadow-1), inset 0 1px 0 rgba(255,255,255,.05);
  isolation: isolate;
  will-change: transform;
}

/* ---- chrome ---- */
/* A floating bar rather than a band with sawn-off ends. The old version was a
   max-width block with its own background, so the gradient stopped dead at the
   content edge on both sides. Rounded, inset, and sitting on the felt like
   every other panel. */
header.top {
  max-width: 68rem; margin: 12px auto 6px; padding: 12px 20px;
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  position: sticky; top: 10px; z-index: 60;
  border: 1px solid var(--glass-line);
  border-radius: 999px;
  background: linear-gradient(180deg, var(--glass-hi), transparent 45%), rgba(14,32,16,.82);
  -webkit-backdrop-filter: blur(14px) saturate(120%); backdrop-filter: blur(14px) saturate(120%);
  box-shadow: var(--shadow-2);
}
@media (max-width: 620px) {
  header.top { border-radius: 18px; padding: 10px 14px; }
}
header.top img.logo { height: 38px; width: auto; display: block; filter: drop-shadow(0 3px 8px rgba(0,0,0,.5)); }
header.top nav { display: flex; gap: 6px; margin-left: auto; font-size: 14px; align-items: center; }
header.top nav a {
  color: var(--cream); text-decoration: none; padding: 6px 11px; border-radius: 999px;
  border: 1px solid transparent;
}
header.top nav a:hover { background: rgba(251,224,164,.10); color: var(--gold-bright); }
header.top nav a.here { border-color: var(--glass-line); background: rgba(255,213,39,.12); color: var(--gold-bright); }
/* Sign out is an icon, not a fourth navigation link: it is the one control up
   here that ends the session, and giving it the same weight as "Clips" is how
   it gets pressed by accident. It is still a real button with a real label —
   the name is on it for anything that does not render the drawing. */
header.top nav .icon-btn {
  display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0;
  margin-left: 4px; border-radius: 999px; border: 1px solid transparent;
  background: transparent; color: var(--tan); cursor: pointer;
  transition: background .14s ease, color .14s ease, border-color .14s ease;
}
header.top nav .icon-btn:hover { background: rgba(251,224,164,.10); color: var(--gold-bright); border-color: var(--glass-line); }
header.top nav .icon-btn:focus-visible { outline: 2px solid var(--glass-line-hot); outline-offset: 2px; }
header.top nav .icon-btn svg { width: 17px; height: 17px; display: block; }
/* ---- the service notice ---- */
/* An annex to the header rather than a banner inside main: it belongs to the
   chrome, not to the page, and every page's own #banner is already spoken for
   by that page's own errors — a queued-clip confirmation writes over it.

   STICKY, like the header it hangs off, and the first version was not — which
   was a real bug rather than a preference. The icon it folds into is inside the
   sticky header and so is reachable at any scroll position; the annex was a
   plain block that had long since scrolled away, so clicking the icon at the
   bottom of a long page expanded something nobody could see. It sticks just
   under the header now, at a --svc-top the script measures, because the header
   wraps to two rows on a narrow screen and its height is not a constant.

   z-index 59, one below header.top's 60, so it slides UNDER the header pill
   rather than colliding with it — and both are above the cards' backdrop-filter
   stacking contexts, the trap that cost the filter bar its dropdown further
   down this file.

   The pill shape stays intact: this is a separate rounded slab underneath, not
   a deformation of a border-radius: 999px bar. */
.svc-annex {
  max-width: 68rem; margin: 0 auto 10px;
  position: sticky; top: var(--svc-top, 76px); z-index: 59;
  border: 1px solid var(--glass-line);
  border-left: 3px solid var(--gold);
  border-radius: 14px;
  background: linear-gradient(90deg, rgba(206,120,6,.18), var(--glass));
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
  box-shadow: var(--shadow-1);
  font-size: 14px;
  /* The fold, animated on max-height, which the SCRIPT sets in pixels from the
     measured content — never here.

     max-height rather than the tidier `grid-template-rows: 1fr -> 0fr`, and the
     reason is the measuring, not the technique: nothing here is guessed, so
     there is no ceiling to clip a three-notice annex and no dead travel to ease
     through on a one-notice one, which is the only real objection to max-height.
     The script measures .svc-inner, which keeps laying out at its full height
     while the annex above it is clipped to zero — the annex's own scrollHeight
     reads 0 once clipped and is useless for reopening.

     A note for whoever verifies this next, because it cost an hour: the
     headless browser pane never paints, so CSS TRANSITIONS NEVER ADVANCE in it.
     A transitioned property stays pinned at its from-value forever, which reads
     as "the element has zero height and the fix does not work" and is nothing
     of the kind. Set `transition: none` before measuring end states there, or
     use a real browser. Same family as the pane never firing scroll events and
     never fetching lazy images. */
  overflow: hidden;
  transition: max-height .26s ease, opacity .18s ease,
              margin-bottom .26s ease, border-width .26s ease, box-shadow .18s ease;
}
.svc-annex.svc-folded {
  opacity: 0; margin-bottom: 0; border-width: 0; box-shadow: none;
  /* Not display:none — that cannot be transitioned. Pointer events go so a
     collapsed annex cannot swallow a click on whatever is behind it, and the
     script adds `inert` so its link and button leave the tab order too. */
  pointer-events: none;
}
.svc-inner {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 11px 16px;
}
.svc-body { flex: 1 1 22rem; min-width: 0; }
.svc-item + .svc-item { margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(251,224,164,.10); }
.svc-item strong { color: var(--gold-bright); }
.svc-when { display: block; margin-top: 3px; font-family: var(--font-mono); font-size: 11.5px; color: var(--tan); }
.svc-actions { display: flex; gap: 8px; align-items: center; margin-left: auto; }
.svc-link, .svc-fold {
  font: inherit; font-size: 13px; padding: 5px 11px; border-radius: 999px;
  border: 1px solid var(--glass-line); background: rgba(251,224,164,.07);
  color: var(--cream); text-decoration: none; cursor: pointer; white-space: nowrap;
}
.svc-link:hover, .svc-fold:hover { background: rgba(255,213,39,.14); color: var(--gold-bright); }
.svc-link:focus-visible, .svc-fold:focus-visible { outline: 2px solid var(--glass-line-hot); outline-offset: 2px; }

/* Tones. Red is broken, blue is planned, gold is working-but-not-for-you. */
.svc-annex.svc-bad   { border-left-color: var(--red);  background: linear-gradient(90deg, rgba(194,60,46,.22), var(--glass)); }
.svc-annex.svc-bad .svc-item strong { color: #ff9c8b; }
.svc-annex.svc-maint { border-left-color: var(--info); background: linear-gradient(90deg, rgba(76,141,255,.20), var(--glass)); }
.svc-annex.svc-maint .svc-item strong { color: #9dc0ff; }
.svc-annex.svc-warn  { border-left-color: var(--gold); }
.svc-annex.svc-info  { border-left-color: var(--tan); background: linear-gradient(90deg, rgba(183,150,114,.16), var(--glass)); }
.svc-annex.svc-info .svc-item strong { color: var(--cream); }

/* The folded state. Coloured rather than tan, because unlike sign-out this icon
   is only present when it has something to say — it is the notice, not a
   control that happens to be there. */
header.top nav .svc-icon.svc-bad   { color: #ff9c8b; }
header.top nav .svc-icon.svc-maint { color: #9dc0ff; }
header.top nav .svc-icon.svc-warn  { color: var(--gold-bright); }
header.top nav .svc-icon.svc-info  { color: var(--tan); }
header.top nav .svc-icon[aria-expanded="true"] { background: rgba(251,224,164,.12); border-color: var(--glass-line); }

/* No rotation on the icon. It was there to tie the icon to the annex for
   someone who did not watch the fold happen, and it cost more than it bought: a
   warning triangle is a shape people read at a glance from its upright
   silhouette, and off-axis it stops looking like a warning and starts looking
   like a rendering fault — on the state that is the FIRST thing a visitor sees,
   since the annex opens expanded. The colour already ties the two together. */

@media (max-width: 620px) {
  .svc-annex { border-radius: 12px; font-size: 13.5px; }
  .svc-inner { padding: 10px 13px; }
  .svc-actions { margin-left: 0; width: 100%; }
}

.who { color: var(--tan); font-size: 12.5px; font-family: var(--font-mono); }
.who::before { content: "◆ "; color: var(--gold); }
main { max-width: 68rem; margin: 0 auto; }

/* Hand-copied into all four public pages and into pageHtml's shell, because this
   app has no shared layout to hang it on — the same arrangement the header is in.
   In pageHtml it goes INSIDE main: body.shell centres main as a grid item, so a
   footer beside it would become a second row and shove the card off centre. */
footer.foot {
  max-width: 68rem; margin: 28px auto 18px; text-align: center;
  font-size: 12.5px; color: var(--tan);
}
/* The build line. Quieter than the status link beside it on purpose: it is
   reference material for the one conversation a week that needs it, not
   something to read every visit. Mono because two dotted version numbers next
   to each other in a proportional face do not line up as a pair. */
footer.foot .build { font-family: var(--font-mono); font-size: 11.5px; opacity: .62; }

.card { padding: 16px 20px; margin: 12px 0; }

.btn {
  display: inline-block; font: inherit; font-weight: 600; font-size: 14px;
  background: linear-gradient(180deg, #E08A0C, var(--gold));
  color: #1a0d00; border: 1px solid rgba(0,0,0,.35); border-radius: 9px;
  padding: 8px 15px; cursor: pointer; text-decoration: none;
  box-shadow: 0 1px 0 rgba(255,255,255,.28) inset, var(--shadow-1);
  transition: transform .12s ease, filter .12s ease;
}
.btn:hover { filter: brightness(1.12); transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; filter: none; }
.btn.ghost {
  background: rgba(251,224,164,.07); color: var(--cream);
  border: 1px solid var(--glass-line); box-shadow: none;
}
.btn.ghost:hover { background: rgba(255,213,39,.14); color: var(--gold-bright); }

.warnbox {
  border-left: 3px solid var(--gold); border-radius: 0 var(--r) var(--r) 0;
  background: linear-gradient(90deg, rgba(206,120,6,.16), var(--glass));
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  padding: 11px 15px; margin: 14px 0; box-shadow: var(--shadow-1);
}
.warnbox.bad { border-left-color: var(--red); background: linear-gradient(90deg, rgba(194,60,46,.20), var(--glass)); }
.warnbox.good { border-left-color: var(--felt); background: linear-gradient(90deg, rgba(40,131,48,.24), var(--glass)); }

/* ---- filters ---- */
/* The filter bar must outrank the content below it, and this is subtler than a
   z-index on the dropdown alone. Every card carries `backdrop-filter`, and that
   creates a stacking context — so a later sibling painted above the dropdown no
   matter how high its z-index was INSIDE the bar. The bar itself has to be a
   positioned, higher-stacked element for its popover to clear the gallery.
   Symptom was the player list disappearing behind the clip thumbnails. */
.filters {
  display: flex; gap: 9px; align-items: center; flex-wrap: wrap;
  padding: 10px 12px; margin: 10px 0 14px;
  position: relative; z-index: 50;
}
.filters input[type=search], .filters select {
  font: inherit; font-size: 14px; padding: 7px 10px; border-radius: 9px;
  background: rgba(0,0,0,.28); color: var(--cream);
  border: 1px solid var(--glass-line);
}
.filters input[type=search] { min-width: 13rem; flex: 1 1 13rem; }
.filters input:focus, .filters select:focus { outline: 2px solid var(--glass-line-hot); outline-offset: 1px; }
.filters select option { background: #14301a; color: var(--cream); }
.f-who { position: relative; }
.f-who summary {
  list-style: none; cursor: pointer; font-size: 14px; padding: 7px 12px; border-radius: 9px;
  border: 1px solid var(--glass-line); background: rgba(0,0,0,.28); color: var(--cream);
}
.f-who summary::-webkit-details-marker { display: none; }
.f-who[open] summary { border-color: var(--glass-line-hot); color: var(--gold-bright); }
.f-who-list {
  position: absolute; z-index: 55; margin-top: 6px; min-width: 12rem; padding: 10px 12px;
  display: grid; gap: 6px; max-height: 60vh; overflow-y: auto;
  /* Opaque, not glass: a translucent popover over clip thumbnails is unreadable. */
  background: #163a1d; border: 1px solid var(--glass-line-hot); border-radius: var(--r);
  box-shadow: var(--shadow-2);
}
.f-who-list label { display: flex; gap: 8px; align-items: center; font-size: 14px; cursor: pointer; white-space: nowrap; }
.f-count { font-size: 12.5px; margin-left: auto; font-family: var(--font-mono); }
.f-sort, .f-from, .f-to { font: inherit; }
.f-dates { display: none; gap: 6px; align-items: center; }
.f-dates.on { display: flex; }
.f-dates input[type=date] {
  font: inherit; font-size: 13.5px; padding: 6px 9px; border-radius: 9px;
  background: rgba(0,0,0,.28); color: var(--cream); border: 1px solid var(--glass-line);
  color-scheme: dark;
}

.seg { display: inline-flex; border: 1px solid var(--glass-line); border-radius: 9px; overflow: hidden; }
.seg button {
  /* THE WEIGHT IS ON THE BASE RULE, NOT ON `.on`, AND THAT IS THE FIX.
     `.on` used to add `font-weight: 600`, so the selected tab's text was WIDER
     than the same tab unselected, and pressing List moved the border between
     them several pixels. A control whose two halves are never the same width,
     and which changes width when you use it, is what "uneven kerning" looks
     like from the outside. Both tabs carry the weight now and `.on` is colour
     alone: the geometry is fixed and the state is still obvious.
     7px 15px rather than 7px 13px so this and `.rv-side` — the map-layer tabs,
     two rows away on the same page — are the same control at the same size,
     instead of two toggles that nearly match. */
  font: inherit; font-size: 13.5px; font-weight: 600; padding: 7px 15px; cursor: pointer;
  background: rgba(0,0,0,.22); color: var(--cream); border: 0; border-right: 1px solid var(--glass-line);
}
.seg button:last-child { border-right: 0; }
.seg button.on { background: rgba(255,213,39,.18); color: var(--gold-bright); }

/* ---- latest clips rail ---- */
.rail-scroll {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(19rem, 22rem);
  gap: 14px; overflow-x: auto; padding: 4px 4px 14px; scroll-snap-type: x mandatory;
  scrollbar-color: var(--gold) rgba(0,0,0,.25);
}
.rail-scroll::-webkit-scrollbar { height: 10px; }
.rail-scroll::-webkit-scrollbar-track { background: rgba(0,0,0,.22); border-radius: 999px; }
.rail-scroll::-webkit-scrollbar-thumb { background: var(--gold); border-radius: 999px; }
.rail-item {
  scroll-snap-align: start; overflow: hidden; display: block;
  text-decoration: none; color: var(--text);
  transition: transform .14s ease, border-color .14s ease, box-shadow .14s ease;
}
/* A showcase rather than a strip: one card in the middle at full size, with the
   previous and next visible either side, so it is obvious there is more and
   which way it is going. The inline padding is what lets the first and last
   card reach the middle at all. */
.rail-scroll.centered {
  --card: min(34rem, 74vw);
  grid-auto-columns: var(--card);
  padding-inline: calc(50% - var(--card) / 2);
  scroll-snap-type: x mandatory;
}
.rail-scroll.centered .rail-item {
  scroll-snap-align: center;
  transform: scale(.93);
  transition: transform .35s ease, border-color .14s ease, box-shadow .14s ease;
}
/* The cards either side recede by dimming their CONTENTS, never themselves, and
   the difference is the reason this rule exists at all. `opacity` on the card
   was what it used to say, and an element carrying both `opacity: .55` and a
   `backdrop-filter` is composited as 55% of (blurred backdrop + card) over 45%
   of the SHARP backdrop underneath — so the felt showed through the flanking
   cards crisp and undimmed, and the blur those cards were paying for was
   half-erased by the unfiltered copy of the same pixels. They read as tinted
   glass panes with nothing behind them, which is exactly what they were.
   Dimming .v-media and .v-body instead leaves the card's own glass at full
   strength, so the backdrop-filter composites once and the flanking cards blur
   the table like every other panel. Scale still carries the recede. */
.rail-scroll.centered .rail-item > * {
  opacity: .55; transition: opacity .35s ease;
}
.rail-scroll.centered .rail-item.is-focus { transform: scale(1); }
.rail-scroll.centered .rail-item.is-focus > * { opacity: 1; }
.rail-scroll.centered .rail-item.is-focus:hover { transform: scale(1) translateY(-3px); }

/* A rail steered by dots has no need of a scrollbar telling you the same thing
   in pixels. It is still scrollable by wheel, drag and keyboard. */
.rail-scroll.dotted { scrollbar-width: none; padding-bottom: 4px; }
.rail-scroll.dotted::-webkit-scrollbar { display: none; }
/* A scroll container clips its children, and it clips their shadows with them:
   setting overflow-x: auto forces overflow-y from `visible` to `auto`, so there
   is no axis left on which the shadow can escape. With .dotted's 4px bottom
   padding the focused card's 22px shadow was therefore cut off dead straight,
   4px under the card and right against the dots — which is what read as
   unpolished on the one section that is above the fold on the front page.
   The only fix available is room INSIDE the padding box: 28px below and 16px
   above clears the resting shadow, and still clears the hover shadow once the
   card has lifted 3px. Measured, not guessed. padding-block only — .centered's
   padding-inline is what centres the first card and must not be touched. */
.rail-scroll.dotted.centered { padding-block: 16px 28px; }
/* align-items, and not align-self on the arrows. The row's height is set by the
   22px arrows, and `align-items` defaults to `normal` — which for items with an
   explicit height behaves as flex-start. So the 9px dots sat flush with the top
   of the row while the arrows centred themselves, putting the two 6.5px out of
   line. Centring the container is what actually puts them on one line. */
.rail-dots { display: flex; justify-content: center; align-items: center; gap: 8px; margin: 2px 0 10px; }
.rail-dots button {
  width: 9px; height: 9px; padding: 0; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--glass-line); background: rgba(0,0,0,.32);
  transition: transform .16s ease, background .16s ease, border-color .16s ease;
}
.rail-dots button:hover { border-color: var(--glass-line-hot); }
.rail-dots button[aria-current="true"] {
  background: var(--gold-bright); border-color: var(--gold-bright); transform: scale(1.3);
}
/* The arrows share the dots' row, so they have to opt out of the 9px circle the
   rule above gives every button in it. They never take aria-current — paging is
   what they do, not where you are — so the lit-dot treatment cannot reach them. */
.rail-dots .rail-arrow {
  width: 22px; height: 22px; font-size: 15px; line-height: 1;
  color: var(--cream);
}
.rail-dots .rail-arrow:hover { background: rgba(255,213,39,.14); color: var(--gold-bright); border-color: var(--glass-line-hot); }
.rail-item:hover { transform: translateY(-3px); border-color: var(--glass-line-hot); box-shadow: var(--shadow-2); }
/* --shadow-2 reaches 58px below the card, and no amount of padding that leaves
   this rail looking like a rail will contain that. A shadow inside a scroll
   container has to be sized to the room it has, or it goes back to being cut off
   straight — so the hero rail hovers with a shadow that fits the 28px it was
   given rather than one that gets trimmed at the bottom. */
.rail-scroll.centered .rail-item:hover { box-shadow: 0 8px 20px rgba(0,0,0,.42); }
.rail-item .v-body { padding: 10px 13px; }
.rail-item:hover .v-media .play span { background: var(--gold-bright); }

/* video frames */
.v-media { position: relative; aspect-ratio: 16/9; background: #0d1f10; display: block; }
.v-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.v-media iframe.yt, iframe.yt { width: 100%; height: 100%; border: 0; display: block; }
.v-media .play {
  position: absolute; inset: 0; display: grid; place-items: center;
  background: radial-gradient(closest-side, rgba(0,0,0,.15), rgba(0,0,0,.5));
}
.v-media .play span {
  width: 54px; height: 54px; border-radius: 999px; display: grid; place-items: center;
  background: rgba(206,120,6,.92); color: #1a0d00; font-size: 20px; padding-left: 4px;
  box-shadow: var(--shadow-2); border: 1px solid rgba(0,0,0,.35);
}
.vid:hover .v-media .play span { background: var(--gold-bright); }
/* A lineup thumbnail is frame zero of a throw, and the crosshair sitting in the
   middle of that frame IS the information — where you point before you throw is
   the whole lineup. Centring a 54px button on it and dimming the image behind
   it is right for a highlight, where the thumbnail is only an invitation, and
   destroys a lineup. So: no overlay, and the affordance moves to the bottom
   right, which on these clips carries the weapon strip and nothing you have to
   reproduce. The radar inset (top left) and the crosshair inset (top right) are
   both left alone deliberately. */
.v-media .play.corner { background: none; place-items: end end; padding: 8px; }
.v-media .play.corner span {
  width: 34px; height: 34px; font-size: 13px; padding-left: 3px;
  background: rgba(206,120,6,.82);
}
.vid:hover .v-media .play.corner span { background: var(--gold-bright); }
.v-title { font-weight: 600; font-size: 13.8px; line-height: 1.3; }
.v-meta { color: var(--muted); font-size: 12px; margin-top: 3px; font-family: var(--font-mono); }

/* ---- match list ---- */
.matches { display: grid; gap: 10px; }
.match {
  display: grid; grid-template-columns: 1fr auto; gap: 5px 16px;
  /* The two rows that pair a left label with a right value share a baseline, so
     the eye can run straight across the row. Row 1 opts out below: a 20px
     display heading and a badge with its own padding have no baseline worth
     sharing, and forcing one lifts the badge off centre. */
  align-items: baseline;
  padding: 13px 16px; text-decoration: none; color: var(--text);
  transition: transform .14s ease, box-shadow .14s ease, border-color .14s ease;
}
.match:hover {
  transform: translateY(-2px); border-color: var(--glass-line-hot); box-shadow: var(--shadow-2);
}
/* Two columns, three rows, every cell placed by hand. What is across from what
   is the whole point of the row — the date reads across from the clip counts, the
   match type across from the checksum — and that only holds if the pairs share a
   grid row. It did not when each column was its own block flow: the 32px map
   heading and the 25px badge beside it are different heights, so every line
   under them was out of step by the difference.
   Explicit placement rather than auto-flow because plenty of rows have no type,
   no checksum, or neither, and auto-placement would slide the survivor into the
   empty cell — the left column's slot, on the wrong side of the row. */
.match .m-map { grid-area: 1 / 1; font-family: var(--font-display); font-size: 20px; letter-spacing: .035em; text-transform: uppercase; }
.match .m-badges { grid-area: 1 / 2; }
.match > .m-meta { grid-area: 2 / 1; }
.match .m-made { grid-area: 2 / 2; }
.match .m-meta { font-size: 12.5px; color: var(--muted); }
.match .m-badges { align-self: center; }
/* What kind of match, where the checksum used to be. This is the field people
   actually scan a library for — "which of these was FACEIT" — so it sits in the
   left column with the map, the score and the date, and the checksum went to the
   right column with the other things you open a specific row to read.
   Not a .badge: badges on these rows mean STATUS (ready, not ingested, N
   suggested, you sat out), and a pill on every one of eighty rows for a category
   would compete with the ones that are telling you something. Uppercase is safe
   here in a way it is not on .checksum below — this is a display label, not a
   join key. */
.match .m-type {
  grid-area: 3 / 1;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--tan); opacity: .8;
}
/* The checksum, quietly. It is the key everything in this stack is keyed on —
   the daemon, the analytics cache, a clip request — so it is worth being able
   to read one off a row without opening the match, but it is not what anybody
   is scanning the list for. Dimmer than the stats above it, on its own line so a
   16-hex string cannot push the clip counts and the K/D/R around. Sits in the
   right-hand column, on the row below the stats it belongs with. */
.match .m-sum {
  grid-area: 3 / 2;
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  opacity: .62; letter-spacing: .04em; word-break: break-all;
}
/* The right-hand column, by cell rather than by wrapper — .m-right is gone and
   these three are what used to live inside it. Alignment only: .m-made already
   takes its size and colour from .m-meta, and .m-sum sets its own 11px above,
   which a font-size here would quietly override. */
.match .m-badges, .match .m-made, .match .m-sum { text-align: right; }

.badge {
  display: inline-block; font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: .09em; text-transform: uppercase; padding: 3px 9px; border-radius: 999px;
  border: 1px solid var(--glass-line); color: var(--tan); background: rgba(0,0,0,.22);
}
.badge.hot { border-color: rgba(255,213,39,.5); color: var(--gold-bright); background: rgba(255,213,39,.14); font-weight: 600; }
.badge.cold { opacity: .6; }
.badge.away { border-color: rgba(183,150,114,.4); }

/* ---- round strip ---- */
.strip { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0 18px; }
.rnd {
  width: 46px; height: 46px; border-radius: 11px; padding: 0; cursor: pointer;
  font: inherit; display: grid; place-items: center; position: relative; color: var(--cream);
  background: linear-gradient(180deg, var(--glass-hi), transparent 45%), rgba(0,0,0,.3);
  border: 1px solid var(--glass-line);
  box-shadow: var(--shadow-1);
  transition: transform .12s ease, box-shadow .12s ease;
}
.rnd:hover { transform: translateY(-2px); }
.rnd.won { background: linear-gradient(180deg, rgba(255,255,255,.10), transparent 45%), rgba(40,131,48,.62); border-color: rgba(120,220,140,.35); }
.rnd.lost { background: linear-gradient(180deg, rgba(255,255,255,.06), transparent 45%), rgba(194,60,46,.30); }
.rnd.active { outline: 2px solid var(--gold-bright); outline-offset: 2px; }
.rnd .k { font-weight: 700; font-size: 15px; }
.rnd .n { font-size: 9.5px; opacity: .72; font-family: var(--font-mono); }
.rnd .flame { position: absolute; top: -7px; right: -6px; font-size: 13px; filter: drop-shadow(0 1px 2px rgba(0,0,0,.6)); }

/* ---- comms: the stage ---- */
/* The video is the page, so it is the first thing on it and as large as the
   window allows. Width-driven, not height-driven: `aspect-ratio` derives the
   height from the width, and constraining the width is the direction that
   cannot letterbox. The cap is what keeps the round strip on screen - at 68vh
   of viewport height the 16:9 box is exactly as tall as it can be with the
   chips still under it, and on a tall window the container's own 68rem wins
   first anyway. */
.stage { margin: 8px 0 12px; }
.stage-video {
  position: relative; aspect-ratio: 16/9; margin-inline: auto;
  max-width: calc(68vh * 16 / 9);
  background: #0d1f10; border: 1px solid var(--glass-line);
  border-radius: var(--r); overflow: hidden; box-shadow: var(--shadow-2);
}
/* A DESCENDANT rule, because the iframe the YouTube API creates carries no
   class of its own - the app's `iframe.yt` rule, which the click-to-play
   galleries use, would never match it and the player would stay at the API's
   fixed 640x390 inside a wrapper that looked right. */
.stage-video .frame, .stage-video .frame > div, .stage-video iframe {
  width: 100%; height: 100%; border: 0; display: block;
}
/* The gesture. It has to cover the player until it is pressed - a click inside
   the iframe belongs to the iframe and grants this document nothing - and it
   has to be gone straight after, or it eats the clicks YouTube's own controls
   need. */
.stage-play {
  position: absolute; inset: 0; width: 100%;
  display: grid; place-content: center; justify-items: center; gap: 9px;
  background: rgba(9,20,10,.62); border: 0; color: var(--cream);
  cursor: pointer; font: inherit;
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
.stage-play:disabled { cursor: progress; }
.stage-play .disc {
  width: 74px; height: 74px; border-radius: 50%; display: grid; place-items: center;
  background: rgba(255,213,39,.16); border: 2px solid var(--glass-line-hot);
}
.stage-play .disc svg { width: 30px; height: 30px; margin-left: 4px; display: block; }
.stage-play b {
  font-family: var(--font-display); font-weight: 400;
  font-size: 22px; letter-spacing: .04em;
}
.stage-play .lede { font-size: 13px; color: var(--tan); }
.stage-none { margin: 8px 0 12px; }
/* The theatre switch, under the picture it changes. Tight to the video above
   it and generous below, so it reads as belonging to the stage rather than
   heading the round strip that follows. */
.stage-tools { margin: -2px 0 16px; gap: 10px; }

/* ---- comms: rich round chips ---- */
/* Scoped to `.rich` and never to `.rnd`, because the match page draws the same
   chip at 46px and must not grow. Five facts on one square, ranked by how often
   they are read: won or lost is the whole chip's colour (the shared `.won` /
   `.lost` rules above), then WHICH ROUND, then how it ended, then our kills,
   then how many of us were left.

   The round number is the large figure. These chips are navigation first - the
   question they answer most often is "take me to round 14", and a strip you have
   to squint at to find a round is a strip you cannot steer with. The kills are
   an annotation on the round, not the other way round. */
.rnd.rich { width: 54px; height: 54px; display: block; }
.rnd.rich .n {
  position: absolute; left: 0; right: 0; top: 11px;
  font-family: var(--font-mono); font-weight: 500; font-size: 19px;
  line-height: 1; text-align: center; opacity: 1;
}
.rnd.rich .k {
  position: absolute; left: 0; right: 0; bottom: 7px; text-align: center;
  font-weight: 600; font-size: 11px; line-height: 1; opacity: .8;
}
/* The knife chip carries no facts, so it is not `.rich` - but its letter still
   has to match the size of the numbers beside it, or the first chip in the row
   reads as a different kind of control. It centres on the base grid. */
.rnd[data-pre] .n { font-size: 18px; opacity: .82; }
.rnd.rich .why { position: absolute; top: 4px; right: 4px; width: 13px; height: 13px; opacity: .8; }
/* Five segments rather than a number: "three of us left" is a shape you read
   without counting, and it is the one fact here that has a denominator. */
.rnd.rich .life {
  position: absolute; left: 6px; right: 6px; bottom: 3px; height: 3px;
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 2px;
}
.rnd.rich .life i { display: block; border-radius: 1px; background: var(--cream); opacity: .8; }
.rnd.rich .life i.dead { opacity: .16; }

/* ---- the round key ---- */
/* Drawn, not described. Each entry pairs the mark as it appears on a chip with
   the word for it, so reading the key is recognition rather than translation. */
#legend { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 13px; }
.legend-key { display: contents; }
.lg { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
.lg-i { width: 13px; height: 13px; opacity: .8; }
/* The swatches carry the CHIP's own backgrounds rather than flat colours, so a
   change to how a won round looks moves the key with it. */
.sw { display: inline-block; width: 13px; height: 13px; border-radius: 4px; border: 1px solid var(--glass-line); }
.sw.won { background: linear-gradient(180deg, rgba(255,255,255,.10), transparent 45%), rgba(40,131,48,.62); border-color: rgba(120,220,140,.35); }
.sw.lost { background: linear-gradient(180deg, rgba(255,255,255,.06), transparent 45%), rgba(194,60,46,.30); }
/* The kill count is text on the chip, so its swatch is the letter itself. */
.sw.k {
  border: 0; width: auto; height: auto; font-weight: 600; font-size: 11px;
  font-family: var(--font-mono); opacity: .8;
}
.sw.k::before { content: '0K'; }
/* Three of five left, which is the shape the bar makes rather than a colour. */
.sw.bar {
  border: 0; border-radius: 1px; width: 20px; height: 3px;
  background: linear-gradient(90deg, var(--cream) 0 60%, rgba(251,224,164,.16) 60% 100%);
}
.legend-note { opacity: .8; }

/* ---- comms: theatre mode ---- */
/* Landscape on a phone, where the sticky header would otherwise sit over the
   video for the whole match.
   The gate is this CLASS and the media query that sets it lives in vod.html,
   deliberately: there is no `orientation` or `max-height` MEDIA QUERY anywhere
   in this file - every breakpoint here is a `max-width` - and one page's needs
   should not introduce that pattern for every other page to inherit. It also lets a viewer's own choice outrank the screen, which
   a media query alone cannot express. */
/* `overflow: hidden` because the video is fixed and covers the viewport, so
   anything still in normal flow behind it is unreachable AND scrollable - a page
   that scrolls under an opaque fullscreen picture, with a scrollbar narrowing
   the picture by its own width. Everything worth reading in theatre is in the
   drawer, which is fixed too. */
body.theatre { padding: 0; overflow: hidden; }
body.theatre header.top,
body.theatre .svc-annex,
body.theatre .foot,
body.theatre .cm-head,
body.theatre .cm-h2,
body.theatre .stage-tools,
body.theatre .stage-none { display: none; }
body.theatre .stage { margin: 0; }
body.theatre .stage-video {
  position: fixed; inset: 0; z-index: 40;
  /* The desktop cap and centring must both be undone here or the fullscreen
     video is drawn at the width a 68vh box would have had. */
  max-width: none; margin-inline: 0;
  aspect-ratio: auto; border: 0; border-radius: 0; box-shadow: none;
  background: #000; display: grid; place-items: center;
}
/* The largest 16:9 box the screen holds, exactly. `dvh` so a phone's collapsing
   address bar does not leave a band of background under the picture. */
body.theatre .stage-video .frame {
  width: min(100vw, calc(100dvh * 16 / 9));
  height: min(100dvh, calc(100vw * 9 / 16));
}
.hud { display: none; }
body.theatre .hud { display: block; }
.hud-btn {
  position: fixed; z-index: 42; display: grid; place-items: center;
  width: 46px; height: 46px; padding: 0; border-radius: 999px; cursor: pointer;
  background: rgba(9,20,10,.62); color: var(--cream);
  border: 1px solid var(--glass-line);
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
}
.hud-btn svg { width: 22px; height: 22px; display: block; }
/* The exit takes the top-left corner and the round callout slides right of it.
   Top-LEFT because that is where a back control belongs and because the
   top-right band is already YouTube's own overlay buttons plus our gear. */
.hud-exit { left: 10px; top: 10px; }
.hud-prev { left: 10px; top: 50%; transform: translateY(-50%); }
.hud-next { right: 10px; top: 50%; transform: translateY(-50%); }
.hud-gear { right: 10px; top: 10px; }
/* Stacked UNDER the gear rather than beside it. Side by side, the pair spans
   ~102px of the top-right corner - which is exactly where the embed puts its own
   overlay buttons, so two of ours sat on two of YouTube's. A column puts one
   button in that band instead of two, and leaves the corner control where it
   already was rather than moving the one people had learned. */
.hud-full { right: 10px; top: 66px; }
.hud-btn.on { border-color: var(--glass-line-hot); color: var(--gold-bright); }
.hud-now {
  position: fixed; z-index: 42; left: 66px; top: 10px; padding: 6px 12px;
  border-radius: 999px; font-family: var(--font-mono); font-size: 12px;
  color: var(--cream); background: rgba(9,20,10,.62);
  border: 1px solid var(--glass-line);
}
/* Under the drawer once it is open, so they are not controls floating on top of
   a panel that covers them. */
body.theatre.drawer-open .hud-next,
body.theatre.drawer-open .hud-full,
body.theatre.drawer-open .hud-gear { display: none; }
/* In normal flow this is a plain block and inherits nothing. */
body.theatre .drawer {
  position: fixed; z-index: 44; top: 0; right: 0; bottom: 0;
  width: min(390px, 88vw); overflow: auto; margin: 0; padding: 12px;
  background: rgba(9,20,10,.92); border-left: 1px solid var(--glass-line);
  -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px);
  transform: translateX(101%); transition: transform .18s ease;
}
body.theatre.drawer-open .drawer { transform: none; }
.drawer-bar { display: none; }
body.theatre .drawer-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  position: sticky; top: -12px; z-index: 1;
  margin: -12px -12px 10px; padding: 10px 12px;
  background: rgba(9,20,10,.96); border-bottom: 1px solid var(--glass-line);
}
body.theatre .drawer-bar b {
  font-family: var(--font-display); font-weight: 400;
  font-size: 19px; letter-spacing: .04em;
}

/* ---- comms player ---- */
/* A round nobody recorded is DRAWN, not omitted. A missing number in a row of
   numbers reads as "nobody called" when it means "nobody was recording". */
.rnd.uncov { opacity: .3; cursor: not-allowed; box-shadow: none; border-style: dashed; }
.rnd.uncov:hover { transform: none; }
.rnd.trunc .n { text-decoration: line-through; }
.tl {
  position: relative; height: 22px; margin: 12px 0 0; cursor: pointer;
  border-radius: 7px; background: rgba(0,0,0,.34); border: 1px solid var(--glass-line);
  overflow: hidden;
}
.tl-fill { position: absolute; inset: 0 auto 0 0; width: 0; background: rgba(255,213,39,.20); pointer-events: none; }
.tl-mark {
  position: absolute; top: 3px; bottom: 3px; width: 2px; margin-left: -1px;
  background: var(--gold-bright); opacity: .55; pointer-events: none;
}
/* The one mark on this bar that is not a round: where one recording session
   ends and the next begins, which is normal at a recorder restart. */
.tl-mark.sess { top: 0; bottom: 0; width: 3px; background: var(--cream); opacity: .85; }

/* ---- killfeed ---- */
.feed { display: grid; gap: 2px; }
.ev {
  display: grid; grid-template-columns: 52px 1fr auto; gap: 10px; align-items: baseline;
  padding: 6px 10px; border-radius: 9px; font-size: 14px;
  border-left: 3px solid transparent;
}
.ev .t { color: var(--muted); font-family: var(--font-mono); font-size: 12.5px; }
.ev.mine { background: rgba(255,213,39,.13); border-left-color: var(--gold-bright); }
.ev.onme { background: rgba(194,60,46,.16); border-left-color: var(--red); }
.ev.roster:not(.mine):not(.onme) { background: rgba(40,131,48,.16); }
.ev.bomb { background: rgba(206,120,6,.14); border-left-color: var(--gold); font-weight: 600; }
.ev.bomb.defused { background: rgba(90,150,220,.14); border-left-color: #6ba7e8; }
.ev .who { font-weight: 600; }
.tag {
  font-family: var(--font-mono); font-size: 10px; padding: 1px 5px; border-radius: 4px;
  background: rgba(0,0,0,.30); color: var(--tan); margin-left: 5px;
  text-transform: uppercase; letter-spacing: .06em;
}
.tag.hs { background: rgba(255,213,39,.24); color: var(--gold-bright); }

/* ---- pager ---- */
.pager {
  display: flex; align-items: center; justify-content: center; gap: 14px;
  margin: 16px 0 4px; font-size: 13.5px; flex-wrap: wrap;
}
.pager .muted { font-family: var(--font-mono); font-size: 12.5px; }
/* Numbered jumps. Newer/Older still work; these are for "page 9 of 14", which
   is otherwise eight clicks away. Tabular figures so the row does not reflow
   as the digits change under the cursor. */
.pager .pg-nums { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; justify-content: center; }
.pager .pg-n { min-width: 34px; padding: 6px 8px; font-variant-numeric: tabular-nums; }
.pager .pg-n[aria-current="page"] {
  background: rgba(255,213,39,.18); color: var(--gold-bright);
  border-color: rgba(255,213,39,.5); opacity: 1; cursor: default;
}
.pager .pg-gap { color: var(--muted); font-family: var(--font-mono); padding: 0 2px; }
/* The numbers cannot offer every page once there is an elision, so anything
   they cannot reach is typed. */
.pager .pg-jump { display: inline-flex; align-items: center; gap: 6px; }
.pager .pg-jump label { color: var(--muted); font-family: var(--font-mono); font-size: 12.5px; }
.pager .pg-jump input {
  font: inherit; font-size: 13.5px; width: 4.5rem; padding: 6px 8px; border-radius: 9px;
  border: 1px solid var(--glass-line); background: rgba(0,0,0,.3); color: var(--cream);
  font-variant-numeric: tabular-nums;
}
.pager .pg-jump input:focus { outline: 2px solid var(--glass-line-hot); outline-offset: 1px; }

/* ---- the roulette spinner ----
 * The first animation in this stylesheet, and it stays the only one: pages here
 * are slow because /demos walks the library on the NAS, so the honest signal is
 * "still working", not a progress bar we cannot fill.
 *
 * Built from the brand tokens rather than an SVG file on purpose. An
 * /assets/wheel.svg loaded as an <img> gets no page cascade, so felt and cream
 * would have to be hardcoded hex that drifts from :root the first time the
 * palette moves — and its animation would ignore prefers-reduced-motion in a
 * way we could not override from here.
 *
 * Wheel and ball counter-rotate, which is what makes a 18px disc read as a
 * roulette wheel rather than a striped circle.
 *
 * THE TWO PERIODS MUST STAY HARMONIC. The pockets are a repeating conic
 * gradient with 8 segments, so the wheel has 90-degree rotational symmetry and
 * *looks* identical four times per turn: its visual period is 1.6s / 4 = 0.4s,
 * not 1.6s. The ball is a single dot, so its visual period is its whole turn.
 * The pattern a person actually sees repeats at the lowest common multiple of
 * the two.
 *
 * The ball ran at 1.1s until 0.7.13, which put that multiple at 4.4s: the ball
 * drifted through every relative phase against the pockets and the whole thing
 * appeared to stutter and restart every four seconds or so. Reported from a
 * phone, where the loading wheel is the thing you end up staring at. At 1.2s the
 * ball is exactly three pocket-periods, the visible cycle is 1.2s, and it reads
 * as one clean continuous spin.
 *
 * So: keep the ball an integer multiple of 0.4s (the pocket period). 0.8s, 1.2s
 * and 1.6s are all fine; 1.1s and 1.5s are not.
 */
.spinner {
  display: inline-block; width: 18px; height: 18px; flex: none;
  border-radius: 999px; border: 2px solid var(--gold);
  position: relative; overflow: hidden;
  box-shadow: 0 0 0 1px rgba(0,0,0,.35), var(--shadow-1);
}
/* Both animations take their delay from one page-wide clock (`--spin-t`, set by
 * `phaseSpinner` in app.js). Several of these pages rebuild a panel's innerHTML
 * on a poll, which destroys and recreates any wheel inside it - and a fresh
 * element restarts its animation at 0deg, so the wheel visibly jumped every poll
 * even though nothing had gone wrong. A negative delay resumes it mid-turn,
 * wherever the page-wide clock says it should be. Unset, the fallback is 0s and
 * the behaviour is exactly what it was before. */
.spinner::before {                       /* pockets, alternating felt and cream */
  content: ""; position: absolute; inset: 0; border-radius: 999px;
  background:
    radial-gradient(circle at 50% 50%, var(--gold) 0 24%, transparent 25%),
    repeating-conic-gradient(var(--felt-deep) 0 45deg, var(--cream) 45deg 90deg);
  animation: roulette 1.6s linear infinite;
  animation-delay: var(--spin-t, 0s);
}
.spinner::after {                        /* the ball, the other way round */
  content: ""; position: absolute; inset: 0; border-radius: 999px;
  background: radial-gradient(circle at 50% 14%, var(--white) 0 11%, transparent 12%);
  animation: roulette 1.2s linear infinite reverse;
  animation-delay: var(--spin-t, 0s);
}
@keyframes roulette { to { transform: rotate(360deg); } }
/* The wheel sits beside its label, never instead of it — the text is what a
   screen reader gets, which is why the wheel itself is aria-hidden. */
p.loading { display: flex; align-items: center; gap: 9px; }

/* The sign-in wheel. Same element, turned up: the gradients are all in
   percentages, so the only thing that does not scale on its own is the rim. */
.spinner.xl { width: 148px; height: 148px; border-width: 5px; }

/* Shown once per sign-in, over the whole page, and gone in under three
   seconds. It is deliberately not a loading state — the page behind it is
   already rendering — so nothing waits on it and dismissing it early costs
   nothing. */
.splash {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-content: center; justify-items: center; gap: 22px;
  background-color: #12401b;
  background-image: var(--felt-stack);
  background-size: var(--felt-stack-size);
  background-repeat: var(--felt-stack-repeat);
  transition: opacity .5s ease;
}
.splash.out { opacity: 0; pointer-events: none; }
.splash p {
  font-family: var(--font-display); font-size: 26px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--gold-bright); margin: 0;
  text-shadow: 0 0 22px rgba(255,213,39,.35);
}

/* ---- stat strip ---- */
.stat-strip { display: flex; gap: 10px; flex-wrap: wrap; margin: 12px 0 4px; }
.stat { padding: 10px 16px; flex: 1 1 10rem; }
.stat b { display: block; font-family: var(--font-display); font-size: 24px; letter-spacing: .035em; color: var(--gold-bright); line-height: 1.1; }
.stat b.with-spin { display: flex; align-items: center; gap: 9px; }
/* Direct child only: the wheel is a <span> too, and a label rule that also
   claims it is a rule waiting to do something surprising to it. */
.stat > span { font-size: 11px; color: var(--muted); font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .09em; }

/* ---- home hero ---- */
.hero { text-align: center; padding: 26px 0 10px; }
.hero img { width: min(420px, 70vw); height: auto; display: block; margin: 0 auto; filter: drop-shadow(0 6px 18px rgba(0,0,0,.5)); }
.hero .row { justify-content: center; margin-top: 20px; }

/* ---- scoreboard ---- */
.sb-team + .sb-team { margin-top: 12px; }
.sb-head, .sb-row {
  display: grid; grid-template-columns: 1fr 5.5rem repeat(10, 2.7rem);
  gap: 6px; align-items: baseline; padding: 5px 8px; border-radius: 8px;
}
.sb-head {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--tan); border-bottom: 1px solid var(--glass-line);
}
.sb-head > span:nth-child(n+3), .sb-row > span:nth-child(n+3) { text-align: right; font-variant-numeric: tabular-nums; }
.sb-row { font-size: 14px; }
.sb-row.hr { background: rgba(40,131,48,.20); }
.sb-row.focus { background: rgba(255,213,39,.16); box-shadow: inset 2px 0 0 var(--gold-bright); }
.sb-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Narrow screens drop the columns you can reconstruct or live without — the
   team label, HS%, UADR and MVP — and keep the multi-kills, which are the
   reason anyone opens a scoreboard on a phone. */
@media (max-width: 620px) {
  .sb-head, .sb-row { grid-template-columns: 1fr repeat(7, 2.1rem); }
  .sb-head > span:nth-child(2), .sb-row > span:nth-child(2),
  .sb-head > span:nth-child(6), .sb-row > span:nth-child(6),
  .sb-head > span:nth-child(8), .sb-row > span:nth-child(8),
  .sb-head > span:nth-child(9), .sb-row > span:nth-child(9) { display: none; }
}

/* ---- queue ---- */
.q-job { display: grid; grid-template-columns: 1fr auto; gap: 3px 14px; padding: 9px 0; border-bottom: 1px solid var(--glass-line); }
.q-eta { text-align: right; }
.q-job:last-child { border-bottom: none; }
/* Your own row in a queue that is the whole team's. A gold edge and a little
   inset wash, because the alternative — counting positions in this list against
   the request list further down the page — is what people were doing instead.
   The badge beside the position carries it for anyone not seeing the colour. */
.q-job.q-mine {
  border-left: 3px solid var(--gold); padding-left: 11px; margin-left: -14px;
  background: linear-gradient(90deg, rgba(255,213,39,.07), transparent 40%);
}
.q-kind {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .09em; text-transform: uppercase;
  padding: 2px 7px; border-radius: 999px; border: 1px solid var(--glass-line); color: var(--tan);
}
.q-job strong.running { display: inline-flex; align-items: center; gap: 8px; vertical-align: middle; }
/* The kind modifier is NAMESPACED, and that is the whole fix for a chip that
   rendered as a full-width capsule.
   The markup used to emit `class="q-kind clip"` straight from the job's kind, so
   a clip render's chip also matched `.clip` further down this file - the
   "Requested by you" history ROW, an entirely unrelated component. Same
   specificity, declared later, so it won: the span picked up `display: grid`,
   `grid-template-columns: 1fr auto` and `padding: 11px 0`, kept `.q-kind`'s
   `border-radius: 999px` and border, and became a huge outlined pill with the
   word CLIP in 10px type at its left edge and the empty `auto` track beside it.
   Only `clip` was affected: an upload's kind is `highlight` and a VOD job's is
   `record`, and neither has a bare class anywhere in this file - which is why
   the uploading card always looked right and the rendering one never did. */
/* k-clip and k-highlight are the SAME artifact at two stages — the daemon's
   render job calls it a clip and the upload item it produces calls it a
   highlight — so they wear the same gold. The chips read "clip" either way now
   (see kindLabel in clips.html); this is the half of that fix the stylesheet
   owns, and without it the row visibly changed colour when the work moved from
   one queue to the other. */
.q-kind.k-clip, .q-kind.k-highlight { border-color: rgba(255,213,39,.5); color: var(--gold-bright); }
.q-kind.k-record { border-color: rgba(183,150,114,.5); }
/* The ETA column is an auto track, so it sizes to its own longest line and the
   job's half of the row pays for it. On a phone that leaves the label, the
   progress bar and the sequence caption fighting over what is left while
   "estimated from past renders" sits in comfort - so below the narrow
   breakpoint the row stacks and the ETA reads as an ordinary line under the job
   it belongs to. The same shape .match already uses, for the same reason. */
@media (max-width: 620px) {
  .q-job { grid-template-columns: 1fr; gap: 2px 0; }
  .q-eta { text-align: left; }
}
.bar { height: 6px; border-radius: 999px; background: rgba(0,0,0,.3); overflow: hidden; margin-top: 6px; }
/* Your own request history, once expanded. Bounded rather than trimmed, so
   nothing is hidden and the page cannot grow without limit — but bounded only
   where a bounded box is unambiguous.
   This was the last inner scroll left outside the rule `.um-panel` and
   `.rv-queue` were brought under in 0.26.0/0.26.1: a swipe that starts inside a
   second scrollable region moves the list and a swipe two centimetres left
   moves the page, and on a touch screen nothing on the page says which you are
   about to get. A cursor has a position, a wheel under it and a scrollbar drawn
   where the region is. So the cap applies above 861px and on a phone the
   history simply renders in full and the page scrolls, which is the behaviour
   somebody expects from a list they just expanded. */
@media (min-width: 861px) {
  .scroll-cap { max-height: 340px; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
}
.bar > i { display: block; height: 100%; background: linear-gradient(90deg, var(--gold), var(--gold-bright)); }

/* ---- suggestions ---- */
.rail { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); }
.sugg { padding: 12px 14px; transition: transform .14s ease, border-color .14s ease; }
.sugg:hover { transform: translateY(-2px); border-color: var(--glass-line-hot); }
.sugg.selected { border-color: rgba(255,213,39,.5); box-shadow: var(--shadow-1), 0 0 0 1px rgba(255,213,39,.2) inset; }
.sugg .s-tag { font-family: var(--font-display); font-size: 18px; letter-spacing: .035em; text-transform: uppercase; }
.sugg .s-meta { color: var(--muted); font-size: 12.5px; }

/* A modal is modal. `showModal` blocks INTERACTION with the page behind it and
   says nothing about scrolling, so a wheel or a swipe over the backdrop scrolls
   the page underneath — and on a phone that is what a finger does when the
   dialog itself has reached its end, so the page slides away behind the thing
   you are reading. Every dialog in this app is `showModal`, so this is safe to
   state once for all of them rather than remembered per page. */
html:has(dialog[open]) { overflow: hidden; }

/* ---- request sheet ---- */
/* ⚠ POSITIONED AND STACKED EXPLICITLY, RATHER THAN TRUSTING THE TOP LAYER.
   Reported on Vivaldi/Android: with the viewer open, the sticky header pill
   (z-index 60) and the utility filter bar (z-index 50) painted OVER the modal,
   and the player did not paint at all. A `showModal` dialog is supposed to be
   in the top layer and above both of those whatever their z-index — but both
   of them carry `backdrop-filter`, this dialog carried `will-change: transform`
   (see the glass block above), and a promoted layer next to a backdrop-root
   boundary is exactly where that guarantee has been known to come apart.
   Rather than argue with the compositor: fix the dialog to the viewport and
   give it a z-index above everything this stylesheet uses. It costs nothing on
   a browser where the top layer already worked. */
dialog.sheet { max-width: 33rem; width: 92%; padding: 20px 24px; color: var(--text); box-shadow: var(--shadow-2); }
/* ⚠ `[open]`, ON EVERY ONE OF THESE, AND HERE IS WHAT IT COSTS TO FORGET IT.
   The UA hides a closed dialog with `dialog:not([open]) { display: none }`,
   which is one class and one element — specificity 0,0,1,1. An author rule of
   `dialog.sheet.lineup { display: flex }` is 0,0,2,1 and beats it outright, so
   0.25.2's flex-column shipped a viewer that never went away: `.close()` took
   the `open` attribute off, the top layer and the backdrop went with it, and
   the box stayed on screen — fixed, above everything, holding whatever the
   close path had not emptied. That is the reported "elements floating around"
   and the "large minimap container at the bottom": the leftover was the
   viewer, and the biggest thing in it is a radar.
   It also explains the screenshot with the header pill painted over the
   viewer. That dialog was not open at all — a leftover is not modal, so it has
   no top layer to be above z-index 60 with, and no backdrop to dim anything.
   Anything that positions or displays a dialog is gated on `[open]`. */
dialog.sheet[open] { position: fixed; inset: 0; margin: auto; z-index: 1000; will-change: auto; }
dialog.sheet.confirm { max-width: 23rem; }
/* The lineup viewer. 33rem is right for a form and useless for a video: the
   whole reason this exists is that a 351px card cannot show which grenade is in
   somebody's hand or where exactly the crosshair sits. This matches `main`'s
   own max-width, so on a desktop the player is around 1000px wide instead of
   350 — and on a phone it is the full screen, where the iframe's own fullscreen
   button is the real answer and is already permitted by ytIframe. */
/* A COLUMN THAT FITS THE SCREEN, which it was not.
   68rem of dialog gives a 16:9 player 612px on a 1080p display; the titlebar and
   a 46vh body took the total past the viewport, and `overflow: hidden` on the
   dialog meant the overshoot was simply unreachable — the notes were below the
   bottom of the screen with nothing to scroll. Flex column with a viewport cap
   fixes it at the root: the media gives up height when there is not enough, and
   the body is the only thing that scrolls. */
dialog.sheet.lineup { max-width: 68rem; width: 96%; padding: 0; overflow: hidden; max-height: 92vh; }
dialog.sheet.lineup[open] { display: flex; flex-direction: column; }
/* ⚠ `flex: none`, and the reason is the letterboxing it caused.
   `flex: 0 1 auto` let the player shrink alongside the body, and a 16:9 box
   that has been shrunk vertically is no longer 16:9 — so the frame inside it
   grew black bars down both sides and the lineup got harder to read, which is
   the one thing this dialog exists to make easier. The player is the content;
   the body is what gives way.
   Bounded by WIDTH from a height budget, the same trick the radar uses: the box
   stays exactly 16:9 and simply gets narrower on a short window, so it is never
   letterboxed at any size. On a 1080p desktop the 68rem dialog is the binding
   constraint and this changes nothing. */
dialog.sheet.lineup .lu-media { aspect-ratio: 16/9; background: #0d1f10; display: block;
  flex: none; width: 100%; max-width: calc(62vh * 16 / 9); margin-inline: auto; }
dialog.sheet.lineup .lu-head { display: flex; gap: 12px; align-items: baseline; justify-content: space-between; flex-wrap: wrap; }
dialog.sheet.lineup .lu-title { font-family: var(--font-display); font-size: 24px; letter-spacing: .03em; text-transform: uppercase; }
/* The facts, at the size they matter.
   This line used to be `Ancient · Smoke · from Outside A Main · Jump throw`,
   one 12.5px mono row of equals. You picked the map two clicks ago and it is in
   the URL; where you stand and how you release it are the lineup. So the
   grenade wears its own colour, the origin and the throw are read at body size
   with the origin emphasised, and the map drops to a footnote under them. */
dialog.sheet.lineup .lu-meta { margin-top: 2px; }
.lu-facts { display: flex; flex-wrap: wrap; align-items: center; gap: 7px 10px; }
.lu-kind {
  font-family: var(--font-display); font-size: 13px; letter-spacing: .06em; text-transform: uppercase;
  color: #10240f; padding: 3px 9px 2px; border-radius: 999px; border: 1px solid rgba(0,0,0,.35);
  background: var(--gold-bright);
}
.lu-fact { font-size: 15px; color: var(--cream); font-weight: 600; }
.lu-fact i { font-style: normal; font-weight: 400; color: var(--muted); }
.lu-sub { color: var(--muted); font-size: 12px; font-family: var(--font-mono); margin-top: 4px; }
/* The same seven colours the pins and the key use, so a smoke is one colour
   everywhere on this site rather than three. */
.lu-kind.k-smoke { background: #9fb3c8; }
.lu-kind.k-flash { background: #ffe066; }
.lu-kind.k-molotov { background: #ff6a3d; }
.lu-kind.k-he { background: #e0453a; }
.lu-kind.k-decoy { background: #45d0c8; }
.lu-kind.k-wallbang { background: #b892ff; }
.lu-kind.k-mixed { background: var(--gold-bright); }
dialog.sheet.lineup .lu-nav { display: flex; gap: 8px; align-items: center; }
dialog.sheet.lineup .lu-nav .lu-pos { color: var(--muted); font-size: 12px; font-family: var(--font-mono); }
/* A window, not a scroll. `.lu-media` used to be the first child, which put the
   only way out of a modal BELOW a 16:9 player — on a phone that is most of a
   screen of scrolling to find Close, and the gesture people reach for instead is
   the back button, which leaves the page. Title and Close ride above the video;
   the arrows, Edit and the meta line stay below, because a titlebar with five
   controls in it has stopped being a titlebar.
   Scoped to `.lu-titlebar`, a class only the viewer uses: `#addSheet` shares
   `dialog.sheet.lineup` and draws its own head, and giving it this chrome by
   inheritance would restyle a form nobody asked to change. */
dialog.sheet.lineup .lu-titlebar {
  flex: none;
  display: flex; gap: 12px; align-items: center; justify-content: space-between;
  padding: 10px 14px 10px 18px; border-bottom: 1px solid var(--glass-line);
  background: rgba(0,0,0,.22);
}
dialog.sheet.lineup .lu-titlebar .lu-title { margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
dialog.sheet.lineup .lu-titlebar button { flex: none; }
/* The still that replaces the embed until somebody presses it.
   A lineup thumbnail is frame zero of a throw and the crosshair in the middle of
   it IS the lineup — and an unplayed YouTube iframe draws its own 68px play
   button in that exact spot, which this app cannot restyle across an origin. So
   the frame is ours until the click, wearing the same corner affordance the
   grid has worn since 0.11.1, and the iframe arrives already playing. */
.lu-still { cursor: pointer; height: 100%; }
/* The box is exactly 16:9 again (see `.lu-media` above), so `cover` crops
   nothing off a 16:9 still and `contain` would only matter for the 4:3
   `hqdefault` fallback — which the gallery deliberately crops rather than
   letterboxes. Left as the gallery's `cover`: one treatment, one place. */
/* Top-level and not `dialog.sheet.lineup .lu-still`, because the drill has the
   same problem the viewer had and had kept the defect: its reveal mounted an
   unplayed embed, so YouTube drew its own 68px button over the middle of the
   aim frame — which is the crosshair, which is the answer the card exists to
   show. Scoping this to the dialog is what let the two drift apart. */
.lu-still:hover .play.corner span,
.lu-still:focus-visible .play.corner span { background: var(--gold-bright); }
.lu-still:focus-visible { outline: 2px solid var(--gold-bright); outline-offset: -2px; }
/* The lineup on its map. Bounded rather than full width: this is where the
   throw is, not what you came to watch, and a radar as wide as the player pushes
   the notes off a laptop screen. */
dialog.sheet.lineup .lu-map { --rv-h: 34vh; }
/* Under the video, two columns on anything wide enough to hold them.
   Stacked, the notes start below the map and the position line and are the
   third scroll of a dialog that is already only 8vh short of the screen. Side
   by side they are visible without scrolling at all, which is the difference
   between a notes list people read and one people do not know is there. */
.lu-cols { display: grid; gap: 0 26px; align-items: start; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
@media (max-width: 860px) { .lu-cols { grid-template-columns: minmax(0, 1fr); } }
.lu-cols > div { min-width: 0; }

/* ---------- the edit sheet ---------- */
/* Two columns for the same reason `.lu-cols` is two: eleven stacked fields are
   three scrolls of a dialog that is already 8vh short of the screen, and a Save
   button nobody scrolls to is a Save button nobody presses. Collapses to one on
   a phone, where side-by-side inputs are 40 characters wide each. */
.ed-cols { display: grid; gap: 0 26px; align-items: start; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
@media (max-width: 860px) { .ed-cols { grid-template-columns: minmax(0, 1fr); } }
.ed-cols > div { min-width: 0; }
/* The verbatim Discord caption, which is the reference somebody reads while
   deciding what to type. Mono, because it is quoted text and not prose. */
.ed-caption { font-family: var(--font-mono); font-size: 12px; color: var(--muted);
  margin-bottom: 10px; word-break: break-word; }
/* What is still missing, in the same words the review queue uses. Amber and not
   red: an unfinished row is work, not a fault. */
.ed-gaps { font-size: 12.5px; color: var(--gold-bright); background: rgba(255,213,39,.10);
  border: 1px solid rgba(255,213,39,.28); border-radius: 8px; padding: 7px 11px; margin-bottom: 12px; }
.ed-pins { display: flex; gap: 10px 14px; align-items: center; flex-wrap: wrap;
  border-top: 1px solid var(--glass-line); margin-top: 6px; padding-top: 12px; font-size: 13px; }
.ed-pins b { font-size: 13px; }
.ed-pins span { color: var(--muted); font-family: var(--font-mono); font-size: 12px; }
.ed-pins a { margin-left: auto; }
/* A failed save says so in the same place the successful one does, and colour
   is the only difference — a message that moved would be a second thing to find. */
#edMsg.bad { color: var(--red); }
/* The viewer stopped being a video with a caption under it. It now carries the
   position, the companions thrown from the same spot, and the notes — which
   together are taller than a 16/9 player on a laptop. So the BODY scrolls and
   the media does not: the video stays put while you read past it, rather than
   the whole dialog scrolling the thing you are watching off the top. */
dialog.sheet.lineup .lu-body { padding: 13px 18px 16px; flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
dialog.sheet.lineup h4 {
  font-family: var(--font-display); font-size: 13px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); margin: 16px 0 7px;
}
/* The console line. Monospace and selectable, because the fallback when the
   clipboard is unavailable is for somebody to select it by hand.

   NOT scoped to the dialog, and that was a real defect rather than a tidy-up:
   the practice page has shown this exact markup since 0.15.0 and it rendered
   as an unstyled run of proportional text, because every rule that gave it a
   box was behind `dialog.sheet.lineup` and practice has no dialog. Same
   reasoning as the .lu-clist family below — a class two pages use cannot have
   its appearance owned by one of them. */
.lu-posline { display: flex; gap: 9px; align-items: center; flex-wrap: wrap; }
.lu-posline code {
  font-family: var(--font-mono); font-size: 12px; user-select: all;
  background: rgba(255,255,255,.06); border: 1px solid var(--glass-line); border-radius: 7px;
  padding: 6px 9px; overflow-x: auto; white-space: nowrap; max-width: 100%;
}
.lu-poshint { font-size: 12px; margin-top: 6px; }
.lu-poshint code, .lu-pw code { font-family: var(--font-mono); font-size: 11px; }
.lu-pw { color: var(--red); }

/* "Also thrown from here". Un-scoped for the reason the console line above it
   was: the practice reveal shows this list outside any dialog. The affordance
   is NOT un-scoped with it — only the viewer's companions are clickable, and a
   pointer cursor on the practice page's static list would promise a navigation
   that page deliberately does not do. So the box is shared and the hover
   belongs to the button. */
.lu-alsolist { display: flex; flex-wrap: wrap; gap: 7px; }
.lu-alsoitem {
  display: inline-flex; align-items: baseline; gap: 7px; font: inherit; font-size: 13px; text-align: left;
  background: var(--glass); color: var(--text); border: 1px solid var(--glass-line); border-radius: 9px;
  padding: 6px 11px;
}
button.lu-alsoitem { cursor: pointer; }
button.lu-alsoitem:hover { border-color: var(--gold); color: var(--gold-bright); }
.lu-alsokind { color: var(--muted); font-size: 11px; font-family: var(--font-mono); text-transform: uppercase; }

/* The review tool's Notes section reuses the viewer's list, so those rules have
   to reach outside dialog.sheet.lineup. Moved rather than duplicated: two
   copies of a list style is how one page quietly stops matching the other. */
.lu-clist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.lu-cmeta { color: var(--muted); font-size: 11.5px; font-family: var(--font-mono); }
.lu-ctext { font-size: 13.5px; white-space: pre-wrap; margin-top: 2px; }
.lu-cempty { font-size: 13px; }
/* A caption note came out of the Discord post and nobody here may remove one,
   so it is marked rather than left to look like a teammate wrote it. */
.lu-ncap .lu-ctext { border-left: 2px solid var(--glass-line); padding-left: 9px; }
.lu-crm {
  font: inherit; font-size: 11px; background: none; border: 0; padding: 0 0 0 6px;
  color: var(--muted); cursor: pointer; text-decoration: underline;
}
.lu-crm:hover { color: var(--red); }
.lu-cform { display: flex; gap: 8px; align-items: flex-start; margin-top: 12px; }
.lu-cform textarea {
  flex: 1; font: inherit; font-size: 13.5px; resize: vertical;
  background: rgba(0,0,0,.22); color: var(--text); border: 1px solid var(--glass-line); border-radius: 9px; padding: 8px 10px;
}
.lu-cmsg { display: inline-block; font-size: 12px; margin-top: 6px; min-height: 1.1em; }

/* The viewer heading. Read-only: renaming lives on the review page, because
   this is the surface all five teammates browse. */
.lu-namecol { min-width: 0; }
.rv-notes { margin-top: 18px; }
.rv-notes h4 {
  font-family: var(--font-display); font-size: 13px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); margin: 0 0 7px;
}

/* The add-a-lineup sheet reuses the viewer dialog, so its fields need the form
   styling the review page has. Not a third set of rules: the same ones, reached
   from one more place. */
dialog.sheet.lineup .field { margin-bottom: 12px; }
dialog.sheet.lineup .field label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 5px; }
dialog.sheet.lineup .field input[type=text], dialog.sheet.lineup .field input:not([type]), dialog.sheet.lineup .field select, dialog.sheet.lineup .field input[type=file] {
  width: 100%; font: inherit; font-size: 14px; background: rgba(0,0,0,.22); color: var(--text);
  border: 1px solid var(--glass-line); border-radius: 9px; padding: 8px 10px;
}
dialog.sheet.lineup .field input[type=file] { padding: 7px; }

/* The edit ledger. Sentences, not a diff table: "TheGFactor changed the throw
   from nothing to Run-jump throw" is what somebody wants; the JSON of that same
   fact is something nobody reads twice. */
.rv-hist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.rv-hist li { border-left: 2px solid var(--glass-line); padding-left: 9px; }

/* The practice card. One lineup at a time, so it is a panel rather than a grid
   - and the map is the whole point of it, so it gets the width. */
.pr-card { background: var(--glass); border: 1px solid var(--glass-line); border-radius: 14px; padding: 16px 18px; max-width: 54rem; }
.pr-head { display: flex; gap: 12px; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-bottom: 12px; }
.pr-count { color: var(--muted); font-size: 11.5px; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: .06em; }
.pr-q { font-family: var(--font-display); font-size: 21px; letter-spacing: .02em; margin-top: 3px; }
.pr-q2 { color: var(--cream); font-size: 14px; margin-top: 2px; }
/* The radar itself. Fit to width and not zoomable, deliberately: the question
   is where on the map these two spots are, which is an overview question. */
/* `.pr-map` was the practice page own static radar box. 0.25.0 moved that card
   onto the shared `.rv-map`, and the markup went with it — the rules stayed
   behind, matching nothing. Deleted rather than left: an unused rule is a rule
   somebody restyles, and the guard already asserts the class never comes back. */
.pr-answer:not(:empty) { margin-top: 16px; border-top: 1px solid var(--glass-line); padding-top: 12px; }
/* The reveal carries four sections now rather than one heading, so the h4s
   need rhythm between them - matching dialog.sheet.lineup h4, which solved the
   same problem for the viewer. */
.pr-answer h4 { font-family: var(--font-display); font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin: 16px 0 8px; }
.pr-answer h4:first-child { margin-top: 0; }
.pr-throw { font-size: 14px; margin: 0; }
/* The answer's badge row. Same classes as the viewer's — one grenade is one
   colour and one throw reads the same wherever it is written — with the gap
   opened a little because these sit under a heading rather than under a title. */
.pr-facts { margin-bottom: 8px; }
.pr-facts .lu-fact { font-size: 14px; }
/* The question card's chip. `.pr-q` is 21px display type and the chip is 13px,
   so it needs its own baseline nudge or it rides high on the line. */
.pr-q .lu-kind { vertical-align: 2px; margin-right: 4px; }
.pr-ytlink { font-size: 12.5px; margin-top: 6px; }
.pr-media { aspect-ratio: 16/9; background: #0d1f10; border-radius: 11px; overflow: hidden; }
.pr-media iframe { width: 100%; height: 100%; border: 0; display: block; }
/* Choosing the deck. The ledger line sits under each lineup so the choice can
   be "the ones I have not touched in a fortnight" instead of a guess - it is
   the only thing that survives a session, and this list is its only reader. */
.pr-picks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.pr-picks li { border-radius: 9px; }
.pr-picks li:hover { background: rgba(255,255,255,.04); }
.pr-picks label { display: flex; gap: 10px; align-items: flex-start; padding: 7px 9px; cursor: pointer; }
.pr-picks input { margin-top: 3px; flex: none; }
.pr-pk { display: flex; flex-direction: column; gap: 1px; }
.pr-pkt { font-size: 14px; }
.pr-pkm { color: var(--muted); font-size: 12px; font-family: var(--font-mono); }
.pr-hidden { font-size: 12.5px; margin: -4px 0 10px; }
.pr-orderlbl { font-size: 13px; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; white-space: nowrap; }
/* The run card, side by side once there is room.
   Stacked, the radar pushed the two answer buttons below the fold on a laptop:
   the one thing a drill card asks you to do needed a scroll to reach, every
   card, and the map you were reading scrolled away as you went for it. */
.pr-run { display: grid; gap: 16px; align-items: start; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); }
@media (max-width: 860px) { .pr-run { grid-template-columns: minmax(0, 1fr); } }
.pr-run > div { min-width: 0; }
#prMap { --rv-h: 56vh; }
.pr-card { max-width: 68rem; }
@media (max-width: 620px) { .pr-card { padding: 13px 12px; } .pr-q { font-size: 18px; } #prMap { --rv-h: 42vh; } }
.rv-hwho { font-size: 12px; font-family: var(--font-mono); color: var(--gold-bright); }
.rv-hwhen { font-size: 11.5px; font-family: var(--font-mono); color: var(--muted); margin-left: 6px; }
.rv-hwhat { font-size: 13px; margin-top: 2px; }

@media (max-width: 620px) {
  dialog.sheet.lineup { width: 100%; max-width: 100%; border-radius: 0; }
  dialog.sheet.lineup .lu-title { font-size: 20px; }
}
/* ---- the match picker ---- */
/* Narrower than the lineup viewer, which is 68rem because it holds a video. A
   match row is text, and at 68rem the date and the badges end up a screen-width
   apart from the map name they belong to.

   The head does NOT scroll and the list does. Filtering a long list means typing
   and then reading, and a search box that scrolls off the top turns a second
   guess into a scroll back up. Same reasoning as the lineup viewer keeping its
   video still while the body moves. */
dialog.sheet.picker { max-width: 52rem; width: 96%; padding: 0; overflow: hidden; }
dialog.sheet.picker .mp-head { padding: 16px 18px 0; }
dialog.sheet.picker .mp-head .filters { margin: 12px 0 0; }
dialog.sheet.picker .mp-list {
  padding: 12px 18px 16px; max-height: min(58vh, 34rem);
  overflow-y: auto; overscroll-behavior: contain;
}
/* `.match` was written for an <a> and these are buttons, because picking one
   changes what this page is playing rather than going anywhere. A button brings
   its own font, colour and centring, and all three have to be handed back. */
.mp-list .match { width: 100%; text-align: left; font: inherit; color: inherit; cursor: pointer; }
.mp-list .match.on { border-color: var(--glass-line-hot); }
/* `1fr` is `minmax(auto, 1fr)`, so a track holding three badges refuses to go
   below their combined width and pushes the row off a phone. These rows carry
   more badges than the ones on /matches.html ever did - up to three, because
   there are three kinds of recording - so both tracks have to be allowed to
   shrink and the badges have to wrap inside their own. */
.mp-list .match { grid-template-columns: minmax(0, 1fr) minmax(0, auto); }
.mp-list .m-badges { display: flex; flex-wrap: wrap; gap: 5px; justify-content: flex-end; }
.mp-empty { color: var(--muted); font-size: 13.5px; padding: 20px 2px; }
@media (max-width: 620px) {
  dialog.sheet.picker { width: 100%; max-width: 100%; border-radius: 0; }
  dialog.sheet.picker .mp-list { max-height: 62vh; }
  /* Stacked at 620 rather than at the 400 the library rows use, because these
     rows carry up to three badges where those carry one. Measured at 430px
     before this rule: the badge track took 309 of 390 and left the players list
     33px to wrap one name per line, a 292px-tall row. It is not an overflow -
     the grid does exactly what `1fr auto` asks - so nothing but measuring the
     cells would have shown it.

     Every cell named, not `> *`: the placements are class-on-class selectors and
     a universal child selector is a specificity lower, so the cells would keep
     their columns and the one-column rule would only look applied. */
  .mp-list .match { grid-template-columns: 1fr; gap: 3px 0; }
  .mp-list .match .m-map, .mp-list .match .m-badges, .mp-list .match .m-meta,
  .mp-list .match .m-made, .mp-list .match .m-type { grid-area: auto; }
  .mp-list .match .m-badges, .mp-list .match .m-made { text-align: left; }
  .mp-list .m-badges { justify-content: flex-start; align-self: start; margin: 2px 0; }
}
dialog.sheet::backdrop { background: rgba(8,20,10,.72); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
.field { display: grid; gap: 4px; margin: 11px 0; }
.field label { font-size: 12.5px; color: var(--muted); }
.field input, .field select {
  font: inherit; padding: 8px 10px; border-radius: 9px;
  border: 1px solid var(--glass-line); background: rgba(0,0,0,.3); color: var(--cream);
}
.field input:focus, .field select:focus { outline: 2px solid var(--glass-line-hot); outline-offset: 1px; }
.field select option { background: #14301a; }
.row { display: flex; gap: 13px; align-items: center; flex-wrap: wrap; }
details.adv summary { cursor: pointer; color: var(--muted); font-size: 13.5px; margin: 11px 0; }

/* ---- clip rows ---- */
.clip { display: grid; grid-template-columns: 1fr auto; gap: 4px 16px; border-bottom: 1px solid var(--glass-line); padding: 11px 0; }
.clip:last-child { border-bottom: none; }
.state { font-family: var(--font-mono); font-size: 10.5px; text-transform: uppercase; letter-spacing: .09em; }
.state.done { color: #8fd6a4; }
.state.failed { color: #ef8f84; }
.state.live { color: var(--gold-bright); }

/* ---- review workspace ---- */
/* Three panes on a desktop — queue, video and form, radar — and one column on a
   phone, because a radar you cannot see while you click it is not a radar. */
.rv-wrap { display: grid; gap: 14px; grid-template-columns: 15rem minmax(0, 1fr) minmax(0, 22rem); align-items: start; }
/* Sticky, and on desktop it scrolls inside itself too.
   The rule this reverses is the same one `.um-panel` refined in 0.26.0, and for
   the same reason: "no second scrollable region" is about a FINGER, where a
   swipe that starts inside one moves the list and a swipe two centimetres left
   moves the page with nothing on screen to say which. A cursor has a position,
   a wheel under that position and a scrollbar drawn where the region is.
   Sticky alone was not enough here. The queue is a hundred and thirty rows; it
   sticks, but everything past the fold is reachable only by scrolling the PAGE,
   which is the video and the map going off the top of the screen to look at the
   list of what is on them. Below 900px — where `.rv-wrap` stops being three
   columns and the queue stops being sticky at all — it is one column among
   others and the page is the scroll. */
.rv-queue { position: sticky; top: 76px; padding: 8px; }
@media (min-width: 901px) {
  .rv-queue { max-height: calc(100vh - 90px); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; }
}
.rv-q { display: block; width: 100%; text-align: left; font: inherit; font-size: 13px; cursor: pointer;
  padding: 7px 9px; border-radius: 8px; border: 1px solid transparent; background: none; color: var(--text); }
.rv-q:hover { background: rgba(255,213,39,.10); }
.rv-q.on { background: rgba(255,213,39,.18); color: var(--gold-bright); border-color: var(--glass-line-hot); }
.rv-q .rv-sub { display: block; color: var(--muted); font-family: var(--font-mono); font-size: 10.5px; margin-top: 1px; }
.rv-q.done .rv-sub { color: var(--felt); }
/* A ROW WITH A HOLE IN IT, and this rule comes after `.done` on purpose: a row
   can be both, and eleven of them are — ticked as reviewed and still carrying no
   landing pin, so they are not on the map at all. The green "reviewed" subtitle
   was the only thing those rows said about themselves. Amber wins, and the
   subtitle says what is missing rather than that somebody signed it off. */
.rv-q.gap .rv-sub { color: var(--gold-bright); }
.rv-q.gap { border-left: 2px solid rgba(255,213,39,.5); border-radius: 8px; }
.rv-head.rv-gap .rv-headn { color: var(--gold-bright); opacity: 1; }
/* The section header is a control now: it folds its map away. A map that is
   finished still gets a section, arriving shut — "done" and "gone" are
   different statements and the second one loses the way back in. */
.rv-head { display: flex; align-items: baseline; gap: 6px; width: 100%; text-align: left; cursor: pointer;
  border: 0; background: none; font-family: var(--font-display); font-size: 13px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted); padding: 9px 9px 4px; border-radius: 8px; }
.rv-head:hover { background: rgba(255,213,39,.10); color: var(--gold-bright); }
.rv-caret { font-size: 10px; line-height: 1; opacity: .8; }
/* The count sits at the far end and says what is LEFT, not what there is: the
   number a reviewer is working against is the one that reaches zero. */
.rv-headn { margin-left: auto; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0;
  text-transform: none; opacity: .85; }
.rv-media { aspect-ratio: 16/9; background: #0d1f10; border-radius: var(--r); overflow: hidden; }
.rv-media iframe { width: 100%; height: 100%; border: 0; display: block; }
.rv-side { display: inline-flex; border: 1px solid var(--glass-line); border-radius: 9px; overflow: hidden; }
.rv-side button { font: inherit; font-size: 13.5px; font-weight: 600; padding: 7px 15px; cursor: pointer; border: 0;
  border-right: 1px solid var(--glass-line); background: rgba(0,0,0,.22); color: var(--cream); }
.rv-side button:last-child { border-right: 0; }
/* Same as `.seg`: the weight belongs on the base rule. On `.on` it made the
   selected tab's text wider than the same tab unselected, so the layer tabs
   changed width when you switched floors. */
.rv-side button.on { background: rgba(255,213,39,.18); color: var(--gold-bright); }

/* The radar. Pins are absolutely positioned in percentage terms so they follow
   the image at any width without recomputing on resize. */
/* The outer box holds the aspect ratio and clips; the inner one is what pans
   and zooms. Pins live inside it, so they follow the image for free — and a
   click is measured against the INNER box, whose getBoundingClientRect already
   reflects the transform, so the pin maths needs no zoom term at all. */
.rv-map { position: relative; display: block; border-radius: var(--r); overflow: hidden; background: #0d1f10;
  touch-action: none; cursor: grab;
  /* Bounded by WIDTH even though the constraint is a height. `max-height` on an
     aspect-ratio box shrinks the box and leaves the image overflowing it, so the
     map is either cropped or every pin floats off its callout; `height x ratio`
     keeps the box matching the picture, which is what the percentage positions
     are measured against. `--rv-ar` is published by radarView, `--rv-h` by
     whichever page is doing the bounding. */
  max-width: calc(var(--rv-h, 400vh) * var(--rv-ar, 1)); margin-inline: auto; }
.rv-map.arming { cursor: crosshair; }
.rv-map.panning { cursor: grabbing; }
.rv-mapinner { position: absolute; inset: 0; transform-origin: 0 0; }
.rv-mapinner img { width: 100%; height: auto; display: block; -webkit-user-select: none; user-select: none; -webkit-user-drag: none; }
.rv-zoom { display: flex; gap: 6px; align-items: center; margin-top: 8px; }
.rv-zoom button { font: inherit; font-size: 13px; line-height: 1; padding: 6px 10px; cursor: pointer;
  border-radius: 8px; border: 1px solid var(--glass-line); background: rgba(0,0,0,.28); color: var(--cream); }
.rv-zoom button:hover { background: rgba(255,213,39,.14); color: var(--gold-bright); }
.rv-zoom .rv-zn { color: var(--muted); font-family: var(--font-mono); font-size: 11.5px; min-width: 3.2rem; }
.rv-pin { position: absolute; width: 15px; height: 15px; margin: -7.5px 0 0 -7.5px; border-radius: 999px;
  border: 2px solid rgba(0,0,0,.55); pointer-events: none; box-shadow: 0 1px 4px rgba(0,0,0,.5); }
.rv-pin.land { background: var(--gold-bright); }
.rv-pin.throw { background: var(--info); }
.rv-pin.other { width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px; background: rgba(251,224,164,.45); border-width: 1px; }
.rv-pin.other.near { background: var(--cream); box-shadow: 0 0 0 4px rgba(255,213,39,.35); }
.rv-legend { color: var(--muted); font-size: 12px; font-family: var(--font-mono); margin-top: 7px; }
/* The snap toggle, in the pin control strip. Sits with the Set landing / Set
   throw buttons because it changes what THEY do, not what the map shows. */
.rv-snapoff { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; white-space: nowrap; }

.rv-snap { color: var(--gold-bright); font-size: 12.5px; min-height: 1.2em; margin-top: 5px; }
/* The position field's hint line. It carries three things at once — what the
   library wants pasted, what is wrong with what WAS pasted, and a one-click way
   to turn a position into a map pin — so it needs a warning colour and a button
   small enough to sit inside a sentence. */
.rv-snap code { font-family: var(--font-mono); font-size: 11.5px; background: rgba(255,255,255,.06); padding: 1px 4px; border-radius: 4px; }
.rv-snap .bad { color: var(--red); font-weight: 600; }

/* ---- the utility map view ---- */
/* The browse page draws PLACES, not lineups: six smokes into A site are six
   cards and one icon, and the icon is what somebody opening a map is looking
   at. So a pin here carries a count and is a button, which is two things the
   review page's pins are not — hence a modifier rather than a change to
   `.rv-pin`, whose own page has to keep hit-testing the map underneath. */
.rv-map.rv-static { cursor: default; touch-action: auto; }
.rv-pin.hit { pointer-events: auto; cursor: pointer; }
.rv-pin.spot {
  width: 19px; height: 19px; margin: -9.5px 0 0 -9.5px;
  display: grid; place-items: center;
  font-family: var(--font-mono); font-size: 10px; font-weight: 700; line-height: 1;
  color: #10240f; text-shadow: none;
}
/* Shape is the second channel, because colour alone excludes anybody who
   cannot separate the orange from the red — and those two are molotov and HE,
   which is exactly the pair you must not confuse. Landing is a circle, a throw
   spot is a diamond. */
.rv-pin.thrown { border-radius: 3px; transform: rotate(45deg); background: var(--info); }
.rv-pin.thrown > span { transform: rotate(-45deg); display: block; }
.rv-pin.hit:hover { box-shadow: 0 0 0 4px rgba(255,213,39,.4); z-index: 3; }
.rv-pin.sel { box-shadow: 0 0 0 4px rgba(255,213,39,.72); z-index: 4; }
/* Everything the filter or the selection is not talking about stays visible and
   stops competing. Hidden would be worse: a map that silently drops half its
   icons when you pick a grenade type is a map you cannot trust. */
.rv-pin.dim { opacity: .34; }
.rv-links { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.rv-links line { stroke: rgba(255,213,39,.62); stroke-width: 2; stroke-dasharray: 5 4; }
.rv-links line.faint { stroke: rgba(251,224,164,.28); stroke-dasharray: 3 5; stroke-width: 1.5; }

/* Grenade colours. Smoke is slate, flash is yellow, molotov orange, HE red,
   decoy teal, wallbang violet — and a place several kinds land in is gold,
   which is also the colour of "no single kind", because an execute has none. */
/* The legend has to be the same colour as the thing it explains, which sounds
   obvious and was not: these rules were written as `.rv-pin.k-smoke` and the key
   below the map draws `<i class="k-smoke">` with no `.rv-pin` on it, so every
   swatch rendered as an empty outline. Paired selectors rather than dropping to
   a bare `.k-smoke`, because that loses to `.rv-pin.land` on specificity and
   would silently turn every practice landing pin gold again. */
.rv-pin.k-smoke, .um-key i.k-smoke { background: #9fb3c8; }
.rv-pin.k-flash, .um-key i.k-flash { background: #ffe066; }
.rv-pin.k-molotov, .um-key i.k-molotov { background: #ff6a3d; }
.rv-pin.k-he, .um-key i.k-he { background: #e0453a; }
.rv-pin.k-decoy, .um-key i.k-decoy { background: #45d0c8; }
.rv-pin.k-wallbang, .um-key i.k-wallbang { background: #b892ff; }
.rv-pin.k-mixed, .um-key i.k-mixed { background: var(--gold-bright); }

/* ---- the browse header ---- */
/* One row: the heading, the map picker, and the two buttons. The picker used to
   own a whole row of its own, and collapsed it is a 22rem chip reading
   "Ancient / 17 lineups - change map" — a label, not a section, and a label that
   pushed the map itself a picker's height down the page for nothing. It takes
   the row back when it is OPEN, because ten buttons IS a section. */
.u-head { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 16px; margin: 30px 0 12px; }
/* h2's own 30px top margin is what clears the sticky header's drop shadow, so
   the row inherits it rather than the heading losing it. */
.u-head h2 { margin: 0; }
.u-head .u-actions { display: flex; flex-wrap: wrap; gap: 10px; margin-left: auto; }
/* The row owns the spacing here. `.mapgrid`'s own margin is what keeps it off
   the paragraph above it on the practice page, and inside this row it would be
   a second gap stacked on the first. */
.u-head .mapgrid { margin: 0; }
.u-head .mapgrid.picked { flex: 0 1 22rem; }
/* Open, it is a section again: a 100% basis wraps it and `order` puts it UNDER
   the row rather than between the heading and the buttons. */
.u-head .mapgrid:not(.picked) { flex: 1 0 100%; order: 3; }
/* Below this the heading and two buttons already fill the line, so a 22rem chip
   beside them is a chip squeezed to nothing. It gets its own row back. */
@media (max-width: 620px) { .u-head .mapgrid.picked { flex: 1 0 100%; order: 3; } }

/* `--um-h` is the height of the map AND the cap on the panel beside it, in one
   place, because those two being the same number is the point of the pairing —
   a panel taller than the map it explains scrolls the map off the top of the
   screen to read the list of what is on it. */
.um-wrap { display: grid; gap: 14px; grid-template-columns: minmax(0, 1.55fr) minmax(17rem, 1fr); align-items: start; --um-h: 62vh; }
@media (max-width: 860px) { .um-wrap { grid-template-columns: 1fr; } }
/* THE SCROLL BOX IS BACK, ON DESKTOP ONLY, AND THAT IS A REFINEMENT OF THE RULE
   RATHER THAN A REVERSAL OF IT.
   What 0.25.3 settled was about a FINGER. A swipe that starts inside a second
   scrollable region moves the list and a swipe two centimetres left moves the
   page, and nothing on screen says which you are about to get — so the panel
   lost its `max-height: 70vh; overflow-y: auto`. A cursor is not a finger: it
   has a position, a wheel under that position, and a scrollbar that appears
   where the region is. None of the three is ambiguous, and hover says which one
   is about to move before anything moves.
   What the static panel cost: Mirage lists 21 places, which was a panel half
   again the height of the map it explains, so reading the list meant scrolling
   the map off the top of the screen. Below 861px it is still one column and
   still static, which is the case the rule was written for. */
#mapRadar { --rv-h: var(--um-h); }
.um-panel { min-width: 0; }
@media (min-width: 861px) {
  .um-panel { max-height: calc(var(--um-h) + 3.4rem); overflow-y: auto; overscroll-behavior: contain;
    scrollbar-width: thin; padding-right: 5px; }
}
/* Six pixels, and it is optical alignment rather than a gap. The panel starts
   with a HEADING, whose cap height sits several pixels below the top of its
   box; the radar starts with the picture's own edge. Aligned on the boxes, they
   read as misaligned — the map sitting proud of the words beside it. */
.um-mapcol { padding-top: 6px; }
@media (max-width: 860px) { .um-mapcol { padding-top: 0; } }
/* A bare <h4>, which nothing in this file styled, so both panel headings came
   out at the browser default — including a 1.33em top margin that was most of
   the gap this pass was asked to close. */
.um-panel h4, .um-rest h4 { margin: 0 0 5px; font-family: var(--font-display); font-weight: 400;
  font-size: 15px; letter-spacing: .035em; text-transform: uppercase; line-height: 1.15; }
.um-hint { color: var(--muted); font-size: 13px; line-height: 1.5; }
.um-panel > .um-hint { margin: 0 0 9px; }
.um-panel > .um-back + h4 { margin-top: 8px; }
/* The second heading in the panel — the lineups off this place that no throw pin
   stands for. Smaller and set apart, because it is a subdivision of the place
   already named above it rather than a new answer to the same question. */
.um-panel h4.um-sub-h { margin-top: 14px; font-size: 13px; color: var(--muted); }
.um-key { display: flex; flex-wrap: wrap; gap: 5px 12px; margin-top: 9px; }
.um-key span { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); font-size: 11.5px; font-family: var(--font-mono); }
.um-key i { width: 10px; height: 10px; border-radius: 999px; border: 1px solid rgba(0,0,0,.5); display: block; }
.um-key i.thrown { border-radius: 2px; transform: rotate(45deg); background: var(--info); }
.um-back { font: inherit; font-size: 12.5px; padding: 0; border: 0; background: none; color: var(--gold-bright); cursor: pointer; }
.um-back:hover { text-decoration: underline; }
/* One line, not two. The name and its count were stacked, which made every row
   42px tall and put eight places on a screen with room for a map's worth. They
   sit on one baseline now and wrap only when the panel is too narrow for both. */
.um-spot { width: 100%; text-align: left; font: inherit; cursor: pointer;
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 1px 10px;
  padding: 7px 10px; margin-bottom: 5px; border-radius: 9px; color: var(--cream);
  background: rgba(251,224,164,.07); border: 1px solid var(--glass-line);
  transition: border-color .12s ease, background .12s ease; }
.um-spot:hover { background: rgba(255,213,39,.14); border-color: var(--glass-line-hot); }
.um-spot.on { background: rgba(255,213,39,.16); border-color: var(--glass-line-hot); }
.um-spot b { font-weight: 600; font-size: 13.5px; }
.um-spot .um-sub { margin: 0 0 0 auto; text-align: right; color: var(--muted); font-size: 11.5px; font-family: var(--font-mono); }
.um-rest { margin-top: 18px; }
/* The map is landing-first, so a lineup with no landing pin has no icon to be.
   Listing those under the map is the difference between "not on the map yet"
   and "not in the library" — and doubles as the nudge to finish the review
   pass, which is the only thing that closes the gap. */
.um-rest .gallery { grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr)); }
.btn.sm { font-size: 12.5px; padding: 3px 9px; border-radius: 7px; vertical-align: baseline; }
.rv-warn { color: var(--red); }
@media (max-width: 900px) {
  .rv-wrap { grid-template-columns: 1fr; }
  .rv-queue { position: static; }
}

/* ---- map picker ---- */
/* Nobody browses utility in the abstract. You open this page because you are
   about to play a map, so the map is the question the page asks first and the
   129-card grid is what you get after answering it. Named `.mapgrid` rather
   than reusing `.seg` because ten options do not belong in a segmented control
   at 375px, and this has to wrap. */
/* `margin-top` because the row above carries Practice, Add a lineup and the
   review link, and 0 put ten map buttons hard against three controls that do
   something entirely different. */
.mapgrid { display: grid; gap: 9px; grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr)); margin: 14px 0 16px; }
/* Once it has been answered it is one button, which is also how you reopen it.
   Ten buttons is the right shape for the question and the wrong shape for the
   answer: on a phone the expanded grid is most of a screen, so you scroll past
   nine maps you did not pick to reach the map you did. */
.mapgrid.picked { grid-template-columns: minmax(0, 22rem); }
.mapgrid button {
  font: inherit; font-size: 14px; font-weight: 600; text-align: left; cursor: pointer;
  padding: 10px 13px; border-radius: 10px; color: var(--cream);
  background: rgba(251,224,164,.07); border: 1px solid var(--glass-line);
  transition: transform .12s ease, border-color .12s ease, background .12s ease;
}
.mapgrid button:hover { background: rgba(255,213,39,.14); color: var(--gold-bright); transform: translateY(-1px); }
.mapgrid button.on { background: rgba(255,213,39,.18); color: var(--gold-bright); border-color: var(--glass-line-hot); }
.mapgrid button .m-n { display: block; margin-top: 2px; font-family: var(--font-mono); font-size: 11px; font-weight: 400; color: var(--muted); }
.mapgrid button.on .m-n { color: var(--gold-bright); opacity: .75; }

/* ---- gallery ---- */
.gallery { display: grid; gap: 14px; grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); }
.vid { overflow: hidden; text-decoration: none; color: var(--text); display: block; cursor: pointer;
       transition: transform .14s ease, border-color .14s ease, box-shadow .14s ease; }
.vid:hover { transform: translateY(-3px); border-color: var(--glass-line-hot); box-shadow: var(--shadow-2); }
.vid.playing { transform: none; }
.vid .v-body { padding: 10px 13px; }

@media (max-width: 620px) {
  body { padding: 0 12px 56px; }
  header.top img.logo { height: 30px; }
  header.top nav { gap: 2px; font-size: 13px; }
  header.top nav a { padding: 5px 8px; }
  /* The header already wraps at this width, and `margin-left: auto` on the nav
     was pushing a wrapped row hard against the right edge while the logo sat
     against the left — a header split to both margins with a hole in the
     middle. Everything centres instead, so each wrapped row is its own line. */
  header.top { justify-content: center; row-gap: 8px; }
  header.top nav { margin-left: 0; width: 100%; justify-content: center; }
  header.top .who { width: 100%; text-align: center; }
  h1 { font-size: 23px; }
  .rail-scroll { grid-auto-columns: 84vw; }
  .ev { grid-template-columns: 46px 1fr; }
  .ev > :last-child { display: none; }
}

/* Match rows give up on two columns when there is no longer room for two.
   `1fr auto` sizes the right column to its widest line — "3 clips · 1 VOD ·
   11/9/5 · 77 ADR" is about 184px and does not shrink — so as the viewport
   narrows it is the LEFT column that pays, and the date and player list are what
   wrap. Measured at 375px: 117px for the left column against 184px for the
   right, and a row half again as tall as it needed to be.
   Below this width they stack into one column instead, in source order, which
   reads down as heading, status, date, what-was-made, type, checksum. The cells
   hand their explicit placement back so auto-flow can do it — one column means
   there is no pairing left to protect, which is the only thing the explicit
   placement was for. 400px rather than the 620px above: two columns are still
   comfortable on a normal phone, and this is meant for the narrow end. */
@media (max-width: 400px) {
  /* Six stacked lines where there were three, so the 5px row gap is now paid five
     times instead of twice. Tightened, or fifteen rows of library become a lot of
     scrolling for whitespace. The 16px column gap goes with the column. */
  .match { grid-template-columns: 1fr; gap: 2px 0; }
  /* Every cell named, rather than `.match > *`. The placements above are
     class-on-class selectors and a universal child selector is a specificity
     lower, so `.match > * { grid-area: auto }` loses to all six of them — the
     cells keep their columns, an implicit second column appears to hold the ones
     placed at 2, and the result is the two-column layout wearing a one-column
     rule. It looks like it worked until you measure it. */
  .match .m-map, .match .m-badges, .match .m-meta,
  .match .m-made, .match .m-type, .match .m-sum { grid-area: auto; }
  /* Right-aligned text in a single column is just text that does not line up
     with the text above it. */
  .match .m-badges, .match .m-made, .match .m-sum { text-align: left; }
  .match .m-badges { align-self: start; }
}
/* ---- score sheet (the highlight-score explainer) ---- */
/* Real tables here, rather than the CSS-grid rows every other list on this site
   uses. A weights table IS tabular - two or three columns, forty-odd rows, read
   by scanning one column down - and .sb-row's fixed track counts would have to
   be redeclared for each of the eight tables on that page. Everything is scoped
   under .sheet so no other page picks up table styling it never asked for. */
.sheet { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.sheet th {
  text-align: left; font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: .09em; text-transform: uppercase; color: var(--muted);
  font-weight: 400; padding: 7px 10px 7px 0; border-bottom: 1px solid var(--glass-line);
}
.sheet td { padding: 6px 10px 6px 0; border-bottom: 1px solid rgba(255,255,255,.05); vertical-align: top; }
.sheet tr:last-child td { border-bottom: none; }
.sheet td:last-child, .sheet th:last-child { text-align: right; padding-right: 0; }
.sheet .w { font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--gold-bright); white-space: nowrap; }
.sheet .w.mult { color: var(--chip-peach); }
.sheet .cond { color: var(--muted); }
.cols2 { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 26px; align-items: start; }
.formula { font-family: var(--font-mono); font-size: 14px; margin: 0; overflow-x: auto; white-space: nowrap; padding-bottom: 3px; }
.formula b { color: var(--gold-bright); font-weight: 400; }
.formula i { color: var(--chip-peach); font-style: normal; }
/* The estimator. A dense auto-fill grid of small numeric inputs - one per
   flourish - because the alternative is twenty stacked .field rows and half a
   page of scrolling for a form nobody would reach the bottom of. */
.calc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 2px 18px; }
.calc-grid .field { margin: 7px 0; }
.calc-grid .field input[type=number] { max-width: 6rem; }
.calc-checks { display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: 6px 18px; margin: 10px 0 2px; font-size: 13.5px; }
@media (max-width: 620px) { .cols2 { grid-template-columns: 1fr; } }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
  .match:hover, .vid:hover, .sugg:hover, .rnd:hover, .btn:hover { transform: none; }
  /* The wheel stops but stays: it degrades to a static roulette icon next to
     the word "Loading…", which is the part that actually carries the state.
     autoScrollRail bails for the same setting, so nothing on these pages moves
     on its own. */
  .spinner::before, .spinner::after { animation: none; }
}
