/* Grit theme — matches frontier's design system (tailwind.config.js tokens). */

/* Self-hosted (Google's own woff2 subsets, committed under static/fonts): no
   third-party request in the critical path, no fonts-CDN privacy footnote.
   unicode-range mirrors Google's split so latin-ext personas still resolve. */
@font-face {
  font-family: 'Rye';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/rye-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Rye';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/rye-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Sanchez';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/sanchez-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: 'Sanchez';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/static/fonts/sanchez-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* This page ships its own dark theme; declaring it lets native form controls,
     scrollbars and focus rings render dark to match instead of defaulting light. */
  color-scheme: dark;
  --grit-bg: #1a1612;
  --grit-bg-light: #2a2420;
  --grit-bg-dark: #0f0d0a;
  --grit-text: #e8dcc8;
  --grit-muted: #a89880;
  --grit-accent: #d4994a;
  --grit-accent-hover: #e5aa5b;
  --grit-danger: #bb4444;
  --steam: #1b2838;
  --steam-hover: #2a475e;
  --line: #3a322a;
}
* { box-sizing: border-box; }
/* One themed ring for keyboard focus everywhere — visible on the dark art
   backgrounds where some UA defaults are not. */
:focus-visible { outline: 2px solid var(--grit-accent); outline-offset: 2px; }
body {
  margin: 0;
  /* Sticky footer: short pages (the splash, empty states) pin the footer to
     the viewport bottom instead of leaving a void beneath it. */
  min-height: 100dvh;
  display: flex; flex-direction: column;
  font-family: 'Sanchez', Georgia, serif;
  /* 16px minimum: form controls inherit this, and iOS auto-zooms any focused
     control under 16px, which is worse than the slightly larger type. */
  font-size: 16px;
  line-height: 1.55;
  color: var(--grit-text);
  /* The game's key art, held at ~3% behind a near-solid wash of the page colour.
     Deliberately barely-there: it should register as texture in the margins, never
     as something competing with a table of numbers. The art is high-contrast — white
     smoke against black — so it punches through far harder than the percentage
     suggests; 7% was already reading as a picture behind the data. The flat colour is layered ON
     TOP of the art rather than the art being faded, so the contrast every text
     rule in this file was chosen against is exactly unchanged. */
  background-color: var(--grit-bg);
  background-image:
    linear-gradient(rgba(26, 22, 18, .972), rgba(26, 22, 18, .972)),
    url('/static/branding/hero.jpg');
  background-size: cover;
  background-position: center top;
  background-attachment: fixed;
  background-repeat: no-repeat;
}
/* iOS renders `fixed` backgrounds at viewport scale and janks while scrolling. */
@media (max-width: 700px) {
  body { background-attachment: scroll; }
  /* The 17-digit SteamID64 column is redundant with the profile link and is what
     forces the scoreboard into sideways scrolling on phones. */
  .col-steamid { display: none; }
}
h1, h2, .font-display, .brand {
  font-family: 'Rye', Georgia, serif;
  font-weight: 400;
}

/* Keyboard users get straight to the data; visually hidden until focused. */
.skip-link {
  position: absolute; left: -9999px; z-index: 10;
  background: var(--grit-bg-dark); color: var(--grit-accent);
  padding: 8px 14px; border-radius: 0 0 6px 0;
}
.skip-link:focus { left: 0; top: 0; }

.site-header, .site-footer {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; row-gap: 8px;   /* long names + nav links fold instead of overflowing */
  padding: 14px 22px;
  /* The game's girder beam under a dark wash — enough that it reads as texture
     behind the nav, not as a picture. The flat colour is the fallback: a missing
     beam costs the texture, never the header. */
  background-color: var(--grit-bg-dark);
  background-image:
    linear-gradient(rgba(15, 13, 10, .55), rgba(15, 13, 10, .55)),
    url('/static/ui/beam.webp');
  background-size: auto, auto 100%;
  background-repeat: repeat, repeat-x;
  background-position: center;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .9);   /* legibility over the beam */
  border-bottom: 1px solid var(--line);
}
.site-footer { border-top: 1px solid var(--line); border-bottom: 0; color: var(--grit-muted); }
.brand { display: flex; align-items: center; gap: 10px; color: var(--grit-accent); text-decoration: none; font-size: 20px; }
.brand img { height: 26px; width: auto; }
nav a { color: var(--grit-text); text-decoration: none; }
/* Lit brass — the game's settings_on treatment for the active/hovered item. */
nav a:hover, nav a[aria-current="page"] {
  color: var(--grit-accent-hover);
  text-shadow: 0 0 8px rgba(229, 170, 91, .6), 0 1px 3px rgba(0, 0, 0, .9);
}
.page-title-row { display: flex; align-items: center; gap: 20px; flex-wrap: wrap; }
.page-tagline { margin: 8px 0 0 4px; color: var(--grit-muted); }
.review-emblem { width: 88px; height: 88px; filter: drop-shadow(0 3px 8px rgba(0, 0, 0, .6)); }

/* Deputy star beside Review. currentColor under an alpha mask, so the glyph
   lights up with the text on hover/current — the same lit-brass treatment.
   A missing asset costs the glyph; the label carries the link. */
.nav-review { display: inline-flex; align-items: center; gap: 7px; }
.nav-icon {
  width: 18px; height: 18px; flex: none;
  background-color: currentColor;
  -webkit-mask: url('/static/ui/star.webp') center / contain no-repeat;
  mask: url('/static/ui/star.webp') center / contain no-repeat;
}

/* Not nav-scoped: the review page's signed-out state uses this in content too. */
a.login {
  background: var(--steam); color: #fff; padding: 7px 14px; border-radius: 5px;
  font-weight: 600; transition: background-color .15s;
  display: inline-block; text-decoration: none;
}
a.login:hover { background: var(--steam-hover); color: #fff; }
.me {
  color: var(--grit-muted);
  /* Steam personas can be 32 chars of anything; don't let one own the header. */
  max-width: 16ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.avatar { height: 24px; width: 24px; border-radius: 4px; border: 1px solid var(--line); }
/* gap, not per-item margins: the avatar and name aren't anchors and were
   butting against the links beside them. */
nav { display: flex; align-items: center; gap: 16px; }

/* The signed-in identity is one control that discloses the account actions. */
.account-menu { position: relative; }
.account-menu summary {
  display: inline-flex; align-items: center; gap: 8px;
  cursor: pointer; list-style: none;
  padding: 4px 9px; border-radius: 5px; border: 1px solid transparent;
}
.account-menu summary::-webkit-details-marker { display: none; }
.account-menu summary:hover,
.account-menu[open] summary { border-color: var(--line); background: var(--grit-bg-light); }
.account-dropdown {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 20;
  margin: 0; padding: 6px 0; list-style: none; min-width: 160px;
  background: var(--grit-bg-dark); border: 1px solid var(--line); border-radius: 6px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, .6);
}
.account-dropdown a { display: block; padding: 8px 14px; }
.account-dropdown a:hover { background: var(--grit-bg-light); }

/* Admin-only env switcher (base.html): a quiet pill naming the split the page
   reads; danger-red once flipped to dev so the state can't be mistaken. */
.env-switch { display: inline-flex; }
.env-pill {
  font: inherit; font-size: 12px; letter-spacing: .8px; text-transform: uppercase;
  cursor: pointer; padding: 3px 11px; border-radius: 999px;
  color: var(--grit-muted); background: transparent; border: 1px solid var(--line);
}
.env-pill:hover { color: var(--grit-accent-hover); border-color: var(--grit-accent-hover); }
.env-pill-dev { color: #fff; background: var(--grit-danger); border-color: var(--grit-danger); }
.env-pill-dev:hover { color: #fff; border-color: var(--grit-text); }
/* Sits between header and content, full-width: nothing below it is live data. */
.env-banner {
  padding: 7px 22px; text-align: center; font-size: 14px;
  color: #fff; background: var(--grit-danger);
  border-bottom: 1px solid var(--line);
}

main { max-width: 900px; margin: 0 auto; padding: 28px 22px; width: 100%; flex: 1; }
h1 { margin-top: 0; color: var(--grit-accent); letter-spacing: .5px; }
h2 { color: var(--grit-text); border-bottom: 1px solid var(--line); padding-bottom: 6px; font-size: 1.15rem; }
/* Section tallies ("Weapons (23)", "Match history (312)") stay quiet next to
   the display-face heading — data, not decoration. */
h2 .count { color: var(--grit-muted); font-family: 'Sanchez', Georgia, serif; font-size: .85em; }
/* Room for the steel button below the card grid without it kissing the next
   section's heading rule. */
.weapons-toggle { margin: 4px 0 20px; }

/* Page hero: a full-bleed band of the game's key art under the site header, with the
   art fading into the page colour so the content below starts on flat ground. Lives
   outside <main> (see base.html) so it can span the viewport while its contents stay
   in the same 900px column as everything else. */
.page-hero {
  position: relative;
  background-image: url('/static/branding/hero.jpg');
  background-size: cover;
  /* 30% down is the riders and the sun; dead centre is mostly dust. */
  background-position: center 30%;
  border-bottom: 1px solid var(--line);
}
/* The scrim. Light at the top so the art reads, solid at the bottom so the band
   dissolves into the page rather than ending on a hard edge. */
.page-hero::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(to bottom,
    rgba(26, 22, 18, .35) 0%,
    rgba(26, 22, 18, .70) 58%,
    var(--grit-bg) 100%);
}
.hero-inner {
  position: relative; z-index: 1;      /* above the scrim */
  max-width: 900px; margin: 0 auto; padding: 22px 22px 18px;
}
.hero-inner h1 { margin: 0; text-shadow: 0 2px 10px rgba(0, 0, 0, .8); }
/* Generic page titles sit on the game's heading plate. NOT the player page:
   its heading is the nameplate, and a plate behind a plate is chrome soup. */
.heading-plate {
  display: inline-block;
  background: url('/static/ui/heading.webp') center / 100% 100% no-repeat;
  /* The art's recessed face is rows 17-119 of 158 (centre 43%); the bottom
     fifth is rail + baked drop shadow. Padding centres the title in the FACE:
     22 + 18 (half the 36px line box) = 40 = 43% of the 92px element. */
  padding: 22px 52px 34px;
}
h1.heading-plate { margin: 0; line-height: 1.2; }
.hero-inner .hero-sub {
  margin: 4px 0 0; color: var(--grit-muted);
  text-shadow: 0 1px 6px rgba(0, 0, 0, .9);
}
/* A page with a hero has already had its top padding from the band. */
.page-hero + main { padding-top: 20px; }

/* Splash for signed-out visitors: one screen — pitch and CTA inside the key-art
   band, a dealt hand of the game's weapon cards filling the band's empty side. */
.welcome-hero { flex: 1; display: flex; }              /* the band takes the whole screen */
.welcome-hero + main { display: none; }                /* nothing below the band */
.welcome-hero .hero-inner { padding: 64px 22px 56px; margin: auto; width: 100%; }
.welcome-inner { display: flex; align-items: center; justify-content: space-between; gap: 44px; }
.welcome-main { max-width: 470px; }
.welcome-pitch { font-size: 18px; text-shadow: 0 1px 6px rgba(0, 0, 0, .85); }
.welcome-note { color: var(--grit-muted); font-size: 14px; text-shadow: 0 1px 4px rgba(0, 0, 0, .9); }

/* Valve's own sign-in button art IS the control — no chrome of ours around
   it (their guidelines forbid combining the logo with other design elements). */
.steam-signin { display: inline-block; line-height: 0; }
.steam-signin img { display: block; }
.steam-signin:hover { filter: brightness(1.15); }

.welcome-cards { display: flex; flex: none; align-items: center; }
.welcome-card { width: 170px; }
.welcome-card .weapon-art { filter: drop-shadow(0 8px 18px rgba(0, 0, 0, .6)); }
.welcome-card:nth-child(1) { transform: rotate(-9deg) translate(16px, 14px); }
.welcome-card:nth-child(2) { transform: translateY(-6px); z-index: 1; }
.welcome-card:nth-child(3) { transform: rotate(9deg) translate(-16px, 14px); }
@media (max-width: 900px) { .welcome-cards { display: none; } }

.stat-picker { margin: 14px 0 20px; }
.stat-picker label { color: var(--grit-muted); margin-right: 8px; }
/* Closed control only: steel face + the game's arrow as the chevron. The open
   dropdown stays native — a custom listbox is an accessibility project, not a
   skin. If arrow-d.webp is missing the face still works; it just loses the glyph. */
.stat-picker select {
  appearance: none; -webkit-appearance: none;
  background-color: var(--grit-bg-light); color: var(--grit-text);
  background-image: url('/static/ui/arrow-d.webp');
  background-repeat: no-repeat; background-position: right 7px center;
  background-size: 20px 20px;
  border: 1px solid var(--line); padding: 7px 34px 7px 11px; border-radius: 6px;
  font-family: inherit;
}
.stat-picker select:hover { border-color: var(--grit-accent); }

/* Tables scroll inside their own container: a compact nameplate plus numeric
   columns is wider than a phone, and the page itself must never pan sideways. */
.table-scroll { overflow-x: auto; }
table.board { width: 100%; border-collapse: collapse; margin: 12px 0; }
table.board th, table.board td { text-align: left; padding: 9px 11px; }
/* Row separators are the game's etched text line, one per row (drawn on the tr
   so it runs unbroken across the columns). A missing asset costs the line, and
   the rows still read from padding + hover. Colour rules below use
   background-color only — the shorthand would clobber the line image. */
table.board tr {
  background: url('/static/ui/line.webp') bottom center / 100% 3px no-repeat;
}
table.board th { color: var(--grit-muted); font-weight: 600; text-transform: uppercase; font-size: 12px; letter-spacing: .5px; }
/* The header row's line is heavier than the data rows' — labels vs data. */
table.board thead tr { background-size: 100% 5px; }
/* Numeric columns right-align (they're already tabular-nums via td). */
table.board .num { text-align: right; }
table.board tbody tr:hover { background-color: var(--grit-bg-light); }
table.board tr.you { background-color: rgba(212, 153, 74, 0.14); }
table.board a { color: var(--grit-text); text-decoration: none; }
table.board a:hover { color: var(--grit-accent); }
.badge {
  background: var(--grit-accent); color: var(--grit-bg-dark); font-size: 11px;
  font-weight: 700; padding: 1px 7px; border-radius: 10px; margin-left: 7px;
  text-transform: uppercase; letter-spacing: .5px;
}

/* htmx marks the hx-indicator target with .htmx-request while a swap is in
   flight; dimming the region is the loading state — no spinner to lay out.
   Dimming (not emptying) also reserves the region's height, so paging never
   collapses the layout mid-swap. */
#board.htmx-request, #matches.htmx-request, #weapons.htmx-request {
  opacity: .45; transition: opacity .15s;
}
button:disabled { opacity: .6; cursor: default; }

.pager { display: flex; gap: 16px; align-items: center; margin-top: 12px; }
.pager a { color: var(--grit-accent); text-decoration: none; }
.pager a:hover { color: var(--grit-accent-hover); }
/* The game's panel arrows, at a 44px touch size, with its hover/pressed art. */
.pager-arrow {
  width: 44px; height: 44px; flex: none; display: inline-block;
  background: center / contain no-repeat;
}
.pager-prev { background-image: url('/static/ui/arrow-l.webp'); }
.pager-prev:hover  { background-image: url('/static/ui/arrow-l-hover.webp'); }
.pager-prev:active { background-image: url('/static/ui/arrow-l-down.webp'); }
.pager-next { background-image: url('/static/ui/arrow-r.webp'); }
.pager-next:hover  { background-image: url('/static/ui/arrow-r-hover.webp'); }
.pager-next:active { background-image: url('/static/ui/arrow-r-down.webp'); }
.page-num { color: var(--grit-muted); }
.empty { color: var(--grit-muted); padding: 22px 0; }

/* Replay actions in the match-history table: compact icon links (play =
   steam:// launch, tray = file download). Scoped under .board to outrank the
   table's own anchor colour; accent so they read as actions, not data. */
td.replay-cell { white-space: nowrap; }
table.board a.replay-action {
  display: inline-block; padding: 8px; line-height: 0;
  color: var(--grit-accent);
}
table.board a.replay-action:hover { color: var(--grit-accent-hover); }
.replay-action svg { width: 18px; height: 18px; fill: currentColor; }

/* 150px min: narrower cells turn the riveted plates into towers when a long
   value (a dev version string) wraps, and the whole grid row stretches with it. */
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 12px 0; }
/* Each stat sits on the game's riveted metal plate (3-slice border-image; the
   sheet is plate.webp, caps 127px, rivet rows 44px — set by export_ui_skin.py).
   The flat colour underneath is the fallback when the sheet is missing. */
.stat-grid div {
  background-color: var(--grit-bg-light); background-clip: padding-box;
  /* Slice 23 = the plate's rivet band (90px at source, sheet is cropped to
     opaque bounds by the export). `round` TILES the edges and face — stretch
     smeared the steel texture across wide cells and streaked the highlights. */
  border: 12px solid transparent;
  border-image: url('/static/ui/plate.webp') 23 fill round;
  padding: 2px 6px;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .7);
}
.stat-grid dt { color: var(--grit-muted); font-size: 12px; text-transform: uppercase; letter-spacing: .5px; }
.stat-grid dd {
  margin: 3px 0 0; font-size: 20px; font-weight: 700; color: var(--grit-text);
  overflow-wrap: anywhere;   /* version strings break instead of towering the cell */
}
.steam-id { color: var(--grit-muted); margin-top: -6px; }
/* IDs sit in the game's keycap outline (from the key-bindings screen), tinted
   brass at export time. Background colour stays as the fallback chrome. */
code {
  background: var(--grit-bg-light); background-clip: padding-box;
  padding: 1px 5px; color: var(--grit-accent);
  border: 6px solid transparent;
  border-image: url('/static/ui/key-rect.webp') 24 fill stretch;
  display: inline-block; line-height: 1.3;
}
a { color: var(--grit-accent); }

/* Match detail: teams scoreboard + events count. */
.team { margin: 16px 0; }
.team h3 { margin: 0 0 4px; color: var(--grit-text); font-family: 'Rye', Georgia, serif; font-weight: 400; font-size: 1rem; }
.team.winner h3 { color: var(--grit-accent); }
.events .count { color: var(--grit-muted); font-size: .9rem; }

/* ---- Skin: the game's own buttons -----------------------------------------
   Brass "bottom button" for primary actions, steel "top button" for quiet ones.
   3-slice border-image (caps 90px / 68px in the sheets, set by export_ui_skin.py)
   so one skin serves any label width; :hover/:active swap in the game's own
   state art. The flat background-color is the fallback: a missing sheet leaves
   a plain readable pill, never a blank button. */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; text-decoration: none;
  font: inherit; font-size: 15px; font-weight: 700; letter-spacing: .3px;
  /* White over the bronze face: 7.4:1 up / 4.6:1 hover. Dark engraved text
     looked the part but measured 2.4:1 on this art. */
  color: #fff; text-shadow: 0 1px 3px rgba(0, 0, 0, .8);
  /* Fallback: an inset brass band the art fully covers. A plain background
     bled through the art's transparent shadow margins — as a pale halo
     unclipped, as strips above/below the face when clipped to padding-box. */
  background: linear-gradient(#8a6532, #8a6532) center / calc(100% + 56px) 58% no-repeat;
  border: 0 solid transparent; border-left-width: 44px; border-right-width: 44px;
  border-image: url('/static/ui/btn-up.webp') 0 90 fill stretch;
  /* The art is bottom-weighted (thick lower rail + baked shadow): the brass
     face centre sits ~10% above the box centre, so the label is nudged up to
     the face's optical centre rather than the geometric one. Measured, not
     eyeballed — face rows 20-63 of 103. */
  height: 50px; padding: 0 2px 10px; min-width: 56px; text-align: center;
}
.btn:hover  { border-image-source: url('/static/ui/btn-hover.webp'); }
.btn:active { border-image-source: url('/static/ui/btn-down.webp'); }
.btn-steel {
  color: var(--grit-text); text-shadow: 0 1px 2px rgba(0, 0, 0, .8);
  background: linear-gradient(var(--grit-bg-light), var(--grit-bg-light)) center / calc(100% + 40px) 62% no-repeat;
  border-left-width: 34px; border-right-width: 34px;
  border-image: url('/static/ui/btn2-up.webp') 0 68 fill stretch;
  height: 44px; padding-bottom: 6px;   /* same optical centring, steel geometry */
}
.btn-steel:hover  { border-image-source: url('/static/ui/btn2-hover.webp'); }
.btn-steel:active { border-image-source: url('/static/ui/btn2-down.webp'); }

.watch-replay-block { margin: 18px 0 24px; }
.replay-note { color: var(--grit-muted); font-size: 14px; margin: 8px 0 0; }

/* The review case is a quiet warm card: the chrome lives in the controls
   (timestamp plates, brass buttons), not wrapped around the form. The
   confirmation-dialog 9-slice was tried here and retired — that art is
   authored for short confirm boxes, and around a full form it became a huge
   cold slab of texture with everything else nested inside it. dialog.webp
   stays exported for an actual confirm dialog, should one exist someday. */
.review-case {
  background-color: var(--grit-bg-light);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 18px 22px 22px;
}

/* Community review (/review): flash messages + verdict form. */
.flash { padding: 10px 14px; border-radius: 6px; margin: 12px 0; }
.flash-success { background: var(--grit-bg-light); border: 1px solid var(--grit-accent); color: var(--grit-text); }
.flash-error { background: var(--grit-bg-light); border: 1px solid #b5453a; color: var(--grit-text); }
.verdict-form { margin-top: 18px; }
/* Radios stay radios (right semantics for one-of-N) presented as chips: the
   whole label is a 44px target, and the checked chip gets the brass ring via
   :has. Browsers without :has still show the themed radio dot, so the checked
   state is never invisible. The fieldset keeps its grouping semantics but
   drops its box — the chips are the structure now. */
.verdict-form fieldset { border: 0; padding: 0; margin: 6px 0 18px; display: flex; flex-wrap: wrap; gap: 10px; }
/* The group label is a legend (announced with each radio), styled like the h3s were. */
.verdict-form legend { font-family: 'Rye', Georgia, serif; color: var(--grit-text); padding: 0; margin-bottom: 8px; }
.verdict-form fieldset label {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 16px; min-height: 44px; cursor: pointer;
  background: var(--grit-bg); border: 1px solid var(--line); border-radius: 7px;
  transition: border-color .15s, background-color .15s;
}
.verdict-form fieldset label:hover { border-color: var(--grit-accent); }
.verdict-form fieldset label:has(input:checked) {
  border-color: var(--grit-accent);
  background-color: rgba(212, 153, 74, .12);
  box-shadow: inset 0 0 0 1px var(--grit-accent);
}
.verdict-form > label { display: block; color: var(--grit-muted); margin-bottom: 2px; }
.verdict-form input[type="radio"] { accent-color: var(--grit-accent); margin: 0; }
/* Darker than the card it sits on, so the field reads as a well, not a patch. */
.verdict-form textarea { width: 100%; max-width: 560px; display: block; margin: 6px 0 14px; background: var(--grit-bg); border: 1px solid var(--line); border-radius: 6px; color: var(--grit-text); padding: 8px 10px; font: inherit; }
/* Buttons are .btn now, which manages its own border-image; no border reset here. */

/* Weapon cards. Rebuilt from the game's own card widget rather than approximated:
   card.json carries each layer's rectangle as a percentage of the 264x372 card, so
   the markup positions them inline and this file only has to style them. The card
   face is light parchment, which is why the rank tint (--rank-colour, set per card)
   can be near-black for a Ten and still read. */
.weapon-cards {
  list-style: none; padding: 0; margin: 14px 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: 14px;
}
.weapon-card {
  background: var(--grit-bg-light); border: 1px solid var(--line);
  border-radius: 10px; padding: 0 0 12px; overflow: hidden;
  display: flex; flex-direction: column;
}
.weapon-art {
  /* aspect-ratio is set inline from the card's own dimensions so the composite is
     never stretched; the layers inside are all absolutely positioned percentages. */
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; position: relative;
}
.card-layer {
  position: absolute;
  /* `contain` everywhere: the layer rectangles are the game's, and the art has to sit
     inside its box rather than be cropped to it. It also protects Dynamite and
     SmokeGrenade, which ship 120x120 HUD icons rather than portrait cards. */
  background-position: center; background-repeat: no-repeat; background-size: contain;
  object-fit: contain;
}
/* The white-with-alpha glyph masks — suit watermark, suit and rank pips. The game
   tints these at runtime, so they're painted as a mask over the rank colour rather
   than as plain images; that's what lets one set of art serve all five ranks. */
.card-layer-tinted {
  background-color: var(--rank-colour, var(--grit-muted));
  -webkit-mask: var(--glyph) center / contain no-repeat;
  mask: var(--glyph) center / contain no-repeat;
}
.weapon-art.is-placeholder::after {
  content: ""; position: absolute; inset: 7px;
  border: 1px dashed var(--line); border-radius: 6px;
}
.weapon-art-fallback {
  position: relative;
  font-family: 'Rye', Georgia, serif; font-size: 34px;
  color: var(--rank-colour, var(--grit-muted)); opacity: .5;
}
.weapon-name {
  font-family: 'Rye', Georgia, serif; font-weight: 400; font-size: 14px;
  margin: 10px 11px 0; color: var(--grit-text); line-height: 1.3;
}
.weapon-meta {
  margin: 3px 11px 0; font-size: 11px;
  text-transform: uppercase; letter-spacing: .5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.weapon-meta-category { margin-bottom: 9px; }
/* Deliberately not colour-coded: the game has no per-suit colour, it tints the whole
   card by rank and says "spades" with artwork. Colouring this red/black here would
   invent a rule the game doesn't have. (Rank text isn't in this line at all — it's
   composed into the title, and tinting it by rank made a Ten near-invisible.) */
.weapon-suit { color: var(--grit-muted); font-weight: 700; }
.weapon-category { color: var(--grit-muted); }

/* Player nameplate: the badge, banner, name and title from the game's own flair,
   laid out as SD_FlairPreview does. The banner is a dark textured plate with its
   motif on the right and no decorative ends, so it's a `cover` background anchored
   right — it takes whatever width the name needs without stretching the art. */
.nameplate {
  display: inline-flex; align-items: stretch; gap: 18px;
  min-width: 0; max-width: 100%; vertical-align: middle;
}
.nameplate-badge {
  width: 92px; height: 92px; flex: none;
  background: center / contain no-repeat;
}
/* Fixed size, like the game's — the plate does NOT grow to fit the name, the name
   shrinks to fit the plate (--name-fit, set per plate server-side). Sizing it to the
   text instead gave every row a different width and cropped the banner art randomly.
   480x92 keeps the banner's own 672x128 proportions and matches the badge height. */
.nameplate-body {
  display: flex; flex-direction: column; justify-content: center;
  width: 480px; height: 92px; max-width: 100%;
  padding: 8px 28px; box-sizing: border-box; overflow: hidden;
  background-size: cover; background-position: center right;
}
/* Banners range from near-black to a bright desert scene, and the text sits directly
   on them, so both lines carry a shadow — without it the gold title vanishes on the
   light ones. Same reason the game's own nameplate fonts are authored with outlines. */
.nameplate-name, .nameplate-title {
  text-shadow: 0 1px 2px rgba(0, 0, 0, .95), 0 0 6px rgba(0, 0, 0, .75);
}
.nameplate-name {
  font-family: 'Rye', Georgia, serif; line-height: 1.2;
  font-size: calc(34px * var(--name-fit, 1));
  color: var(--grit-text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.nameplate-name a { color: inherit; text-decoration: none; }
.nameplate-name a:hover { color: var(--grit-accent-hover); }
.nameplate-title {
  font-size: calc(22px * var(--title-fit, 1));
  text-transform: uppercase; letter-spacing: .6px;
  color: var(--grit-accent);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Table rows. Still half the header size, but doubled alongside it — a scoreboard
   row is now as tall as its badge, so a 100-player match is a long page by design. */
.nameplate-compact { gap: 14px; }
.nameplate-compact .nameplate-badge { width: 56px; height: 56px; }
.nameplate-compact .nameplate-body {
  width: 294px; height: 56px; padding: 5px 18px;   /* same 5.25:1 plate, half scale */
}
.nameplate-compact .nameplate-name { font-size: calc(21px * var(--name-fit, 1)); }
.nameplate-compact .nameplate-title {
  font-size: calc(13px * var(--title-fit, 1)); letter-spacing: .4px;
}

/* Wildcard badge: the game's card art for a match's game_mode. The six chase
   wildcards are 1:1 with the mode Ledger stores, so this is the mode's real identity
   rather than a label — "The Train Chase", not "trainchase". */
.wildcard {
  display: inline-flex; align-items: center; gap: 8px;
  color: var(--grit-text); font-size: 13px;
}
.wildcard-icon {
  width: 28px; height: 28px; flex: none;
  background-color: var(--grit-accent);
  -webkit-mask: var(--glyph) center / contain no-repeat;
  mask: var(--glyph) center / contain no-repeat;
}
/* Table rows: smaller art and a tighter gap, so a mode cell stays one line and the
   rows keep the same height as the numeric columns beside them. */
.wildcard-compact { gap: 6px; }
.wildcard-compact .wildcard-icon { width: 20px; height: 20px; }
/* The mode cell in Recent matches is a link; keep the anchor colour on the label
   rather than letting the badge's own colour win, so it still reads as clickable. */
td a .wildcard { color: inherit; }
/* `auto` top margin pins the stats to the card foot so rows line up across the
   grid even when a weapon name wraps to two lines. */
.weapon-stats { margin: auto 11px 0; display: grid; gap: 2px; }
.weapon-stats div {
  display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  border-top: 1px solid var(--grit-bg-dark); padding-top: 2px;
}
/* nowrap is load-bearing: "Headshot %" wrapping in some cards and not others
   shifted every row below it and broke alignment across the whole grid. */
.weapon-stats dt { color: var(--grit-muted); font-size: 11px; white-space: nowrap; }
.weapon-stats dd {
  margin: 0; font-size: 12px; font-weight: 700; color: var(--grit-text);
  font-variant-numeric: tabular-nums;
}

/* Legal pages. Prose, not data — narrower measure and looser leading than the rest of
   the site, because these are read in paragraphs rather than scanned in rows. */
.legal { max-width: 44rem; }
.legal h2 {
  margin-top: 30px; font-family: 'Rye', Georgia, serif; font-weight: 400;
  font-size: 1.1rem; color: var(--grit-accent);
}
.legal h3 {
  margin: 22px 0 6px; font-size: 1rem; color: var(--grit-text); font-weight: 700;
}
.legal p, .legal li { line-height: 1.7; }
.legal ul { padding-left: 20px; }
.legal li { margin-bottom: 7px; }
.legal-lede {
  border-left: 2px solid var(--grit-accent);
  padding-left: 14px; color: var(--grit-text);
}
.legal-note {
  margin-top: 34px; padding-top: 14px; border-top: 1px solid var(--line);
  color: var(--grit-muted); font-size: 13px;
}
.footer-links a { color: var(--grit-muted); text-decoration: none; }
.footer-links a:hover { color: var(--grit-accent); }

/* --- Admin suppression UI + anonymized match slots ------------------------ */
/* A suppressed player's slot on the scoreboard: present (the match had N
   players) but deliberately mute — no link, no id, no plate. */
.anon-player {
  color: var(--muted, #8a8578);
  font-style: italic;
}

.suppression-search form {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
}
.suppression-search input[type="search"] {
  min-width: 20rem;
}
.suppress-confirm {
  display: grid;
  gap: 0.5rem;
  max-width: 28rem;
  padding: 0.75rem 0;
}
/* An open confirm panel lives inside a results-table cell, and a table cell is
   as narrow as its column. Anchoring the cells to the top keeps the player's
   alias and id level with the form that will destroy them, instead of centred
   against a tall open panel.

   Do NOT give the open panel a min-width to buy it room. That was tried: it
   forces the table wider than .table-scroll, which then clips the action column
   and puts the Steam ID the admin has to re-type behind a horizontal scrollbar,
   truncated mid-digit. Unreadable is worse than cramped when the value is the
   confirmation itself. The panel stays fluid and wraps; the id is allowed to
   break rather than to widen its column. The real fix is issue #20 — take the
   confirm step off the row entirely. */
.suppression-results td { vertical-align: top; }
.suppression-results details[open] { display: block; padding-bottom: 0.5rem; }
.suppression-results .suppress-confirm { min-width: 0; max-width: 100%; }
.suppression-results .suppress-confirm input,
.suppression-results .suppress-confirm .btn { max-width: 100%; }
/* The id is the thing being confirmed: it must be fully readable in place, so
   let it wrap within the column rather than push the column wider. */
.confirm-target { margin: 0; overflow-wrap: anywhere; }
/* Purge destroys the rebuild map and cannot be undone. The panel is marked so
   an admin cannot mistake it for the Restore box directly above it. */
details.danger > summary {
  border-color: var(--grit-danger);
}
details.danger[open] {
  border-left: 3px solid var(--grit-danger);
  padding-left: 0.75rem;
  margin-top: 0.25rem;
}
details.danger .suppress-confirm {
  align-items: start;
}
details.danger .suppress-confirm > .btn,
details.danger .suppress-confirm > a.btn {
  justify-self: start;
}
.suppression-notes ul,
.suppression-detail table {
  max-width: 60rem;
}
/* Scope note under the evidence table: says what a clean scrub does and does
   not cover. Quieter than the numbers above it, but not hidden — it is part of
   what an admin signs off on. */
.suppression-caveat {
  color: var(--grit-muted);
  font-size: 0.9em;
  max-width: 60rem;
}
