/* home.css — the public landing page at /.
 *
 * Standalone rather than layered on shop.css: this page is full-bleed and
 * scroll-driven, where shop.css assumes a padded .shop column. The palette is
 * the same one theme_id 1 uses (see the :root fallbacks in player.css), so the
 * site reads as one thing.
 */

:root {
  --accent: #74ebd5;
  --accent-2: #9face6;
  --on-accent: #06232b;
  --accent-rgb: 116, 235, 213;
  --ink: #eaf6ff;
  --ink-dim: rgba(234, 246, 255, 0.68);
  --ink-faint: rgba(234, 246, 255, 0.45);
  --glass: rgba(255, 255, 255, 0.07);
  --glass-line: rgba(255, 255, 255, 0.16);
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
  /* Anchor targets clear the sticky topbar instead of hiding behind it. */
  scroll-padding-top: 92px;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  color: var(--ink);
  background: #0f2027;
  -webkit-font-smoothing: antialiased;
  /* Deliberately no overflow-x here: `hidden` on body resolves overflow-y to
     auto and makes body its own scroll container, which takes the page scroll
     away from the window and breaks everything wireScroll() measures. The one
     thing that overflows sideways — the hero glow — is clipped by .hero. */
}

/* The site's animated gradient, as a fixed viewport-sized layer rather than
   body's own background. This page is several thousand pixels tall, and an
   animated `background-attachment: fixed` gradient on a page that long repaints
   the whole viewport on every frame *and* on every scroll — which is exactly the
   budget the scroll animations need. One fixed layer costs a viewport repaint
   and nothing else. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  background: linear-gradient(-45deg, #0f2027, #203a43, #2c5364, #1e3c72);
  background-size: 400% 400%;
  animation: gradientShift 18s ease infinite;
}

@keyframes gradientShift {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

/* Scroll progress
   -------------------------------------------------------------------------- */
.scroll-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 3px;
  z-index: 60;
  background: rgba(255, 255, 255, 0.06);
}

.scroll-progress span {
  display: block;
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.7);
}

/* Top bar
   -------------------------------------------------------------------------- */
.topbar {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: 14px clamp(16px, 4vw, 46px);
  background: rgba(10, 26, 34, 0.55);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border-bottom: 1px solid transparent;
  transition: background 0.3s ease, border-color 0.3s ease;
}

/* home.js adds this once the hero is scrolled past. */
.topbar.stuck {
  background: rgba(8, 22, 30, 0.85);
  border-bottom-color: var(--glass-line);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--ink);
  text-decoration: none;
  font-weight: 700;
  font-size: 1.15rem;
  letter-spacing: 3px;
}

.brand img {
  border-radius: 9px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
}

.topnav {
  display: flex;
  align-items: center;
  gap: clamp(8px, 2vw, 26px);
}

.topnav a {
  color: var(--ink-dim);
  text-decoration: none;
  font-size: 0.94rem;
  font-weight: 600;
  padding: 8px 4px;
  transition: color 0.2s ease;
}

.topnav a:hover { color: var(--accent); }

.nav-cta {
  color: var(--on-accent) !important;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  background-size: 200% auto;
  border-radius: 999px;
  padding: 9px 18px !important;
  transition: background-position 0.4s ease, transform 0.15s ease;
}

.nav-cta:hover { background-position: right center; transform: translateY(-1px); }

/* The account circle that closes the topbar is in authslot.css, which every page
   loads — it is one fixed-size circle in both states, so nothing here reflows
   depending on who is looking at the page. It picks up --accent and friends from
   the :root block above. */

/* Hero
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  min-height: calc(100svh - 62px);
  display: grid;
  place-items: center;
  text-align: center;
  padding: 60px 20px 120px;
  overflow: hidden;
}

/* Soft accent bloom behind the wordmark. */
.hero-glow {
  position: absolute;
  width: min(880px, 120vw);
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(var(--accent-rgb), 0.20), transparent 62%);
  filter: blur(10px);
  animation: breathe 9s ease-in-out infinite;
  pointer-events: none;
}

@keyframes breathe {
  0%, 100% { transform: scale(1); opacity: 0.85; }
  50%      { transform: scale(1.12); opacity: 1; }
}

/* home.js parallaxes this as the hero scrolls away. */
.hero-inner {
  position: relative;
  z-index: 1;
  display: grid;
  justify-items: center;
  gap: 18px;
  will-change: transform, opacity;
}

.hero-logo {
  border-radius: 26px;
  box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
  animation: dropIn 0.9s cubic-bezier(0.22, 1, 0.36, 1) both;
}

.wordmark {
  font-size: clamp(4.5rem, 19vw, 11rem);
  line-height: 0.9;
  letter-spacing: clamp(4px, 2vw, 18px);
  font-weight: 800;
  background: linear-gradient(90deg, var(--accent), var(--accent-2), var(--accent));
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: shine 5s linear infinite, dropIn 1s cubic-bezier(0.22, 1, 0.36, 1) 0.1s both;
}

@keyframes shine {
  to { background-position: 200% center; }
}

@keyframes dropIn {
  from { opacity: 0; transform: translateY(26px) scale(0.96); }
  to   { opacity: 1; transform: none; }
}

.expanded {
  font-size: clamp(1rem, 3.4vw, 1.6rem);
  font-weight: 600;
  letter-spacing: clamp(2px, 1vw, 7px);
  text-transform: uppercase;
  color: var(--ink);
  animation: dropIn 1s cubic-bezier(0.22, 1, 0.36, 1) 0.25s both;
}

/* The slogan lands one line at a time — the three spans share this animation on
   staggered delays. Wraps to three rows on narrow screens. */
.slogan {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 6px 16px;
  margin-top: 6px;
  font-size: clamp(0.95rem, 2.4vw, 1.22rem);
  color: var(--ink-dim);
}

.slogan span {
  position: relative;
  padding-left: 18px;
  animation: dropIn 0.8s cubic-bezier(0.22, 1, 0.36, 1) both;
}

.slogan span::before {
  content: '◆';
  position: absolute;
  left: 0;
  color: var(--accent);
  font-size: 0.7em;
  top: 0.25em;
}

.slogan span:nth-child(1) { animation-delay: 0.45s; }
.slogan span:nth-child(2) { animation-delay: 0.65s; }
.slogan span:nth-child(3) { animation-delay: 0.85s; }

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  justify-content: center;
  margin-top: 16px;
  animation: dropIn 0.9s cubic-bezier(0.22, 1, 0.36, 1) 1.05s both;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 13px 26px;
  border-radius: 12px;
  font-weight: 700;
  font-size: 0.98rem;
  text-decoration: none;
  transition: transform 0.15s ease, background-position 0.4s ease, background 0.2s ease;
}

.btn-primary {
  color: var(--on-accent);
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  background-size: 200% auto;
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.4);
}

.btn-primary:hover { background-position: right center; transform: translateY(-3px); }

.btn-ghost {
  color: var(--ink);
  background: var(--glass);
  border: 1px solid var(--glass-line);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
}

.btn-ghost:hover { background: rgba(255, 255, 255, 0.15); transform: translateY(-3px); }

/* Mouse-wheel hint at the bottom of the hero. */
.scroll-cue {
  position: absolute;
  bottom: 34px;
  width: 26px;
  height: 42px;
  border: 2px solid rgba(var(--accent-rgb), 0.55);
  border-radius: 14px;
  display: grid;
  justify-items: center;
  padding-top: 7px;
}

.scroll-cue span {
  width: 4px;
  height: 9px;
  border-radius: 2px;
  background: var(--accent);
  animation: wheel 1.6s ease-in-out infinite;
}

@keyframes wheel {
  0%   { transform: translateY(0); opacity: 1; }
  70%  { transform: translateY(14px); opacity: 0; }
  100% { transform: translateY(0); opacity: 0; }
}

/* Sections
   -------------------------------------------------------------------------- */
.section {
  max-width: 1120px;
  margin: 0 auto;
  padding: clamp(64px, 11vh, 130px) clamp(18px, 4vw, 30px);
}

/* Scroll-in reveal. home.js observes these and adds .in once. */
.reveal {
  opacity: 0;
  transform: translateY(38px);
  transition: opacity 0.8s cubic-bezier(0.22, 1, 0.36, 1),
              transform 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}

.reveal.in { opacity: 1; transform: none; }

.eyebrow {
  font-size: 0.78rem;
  letter-spacing: 3px;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 10px;
  font-weight: 700;
}

.section-title {
  font-size: clamp(1.7rem, 4.6vw, 2.5rem);
  font-weight: 800;
  letter-spacing: 0.5px;
}

.section-lead {
  margin-top: 10px;
  color: var(--ink-dim);
  max-width: 60ch;
  line-height: 1.6;
}

.section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
}

.placeholder {
  grid-column: 1 / -1;
  color: var(--ink-faint);
  padding: 34px 0;
}

/* The right-hand end of a section head that carries more than one control —
   only the roster's, holding the search and the refresh button. `flex-start`,
   not the head's own `flex-end`, so the input and the button line up along
   their tops; their sub-labels (the count and the status) are different heights
   and bottom-aligning them would stagger the two controls. */
.section-tools {
  /* Also the anchor for `.refresh-status` — see the note there. */
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 12px;
}

/* Refresh — see "The refresh button" in home.js. Scoped to the whole page, not
   to the roster it sits in; it is in that head, beside the search, because the
   topbar is measured full.
   -------------------------------------------------------------------------- */
.refresh-box {
  display: grid;
  justify-items: end;
}

.refresh-btn {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 11px 20px;
  border-radius: 999px;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  background: var(--glass);
  border: 1px solid var(--glass-line);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  transition: background 0.2s ease, transform 0.15s ease, border-color 0.2s ease;
}

.refresh-btn:hover { background: rgba(255, 255, 255, 0.16); }
.refresh-btn:active { transform: scale(0.96); }

.refresh-btn:focus-visible {
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.55);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.16);
}

/* Disabled means one of two things now — fetching, or the minute's cooldown
   after a refresh that landed. Dimming is what separates either from idle; the
   spin below is what separates them from each other. `player.css` and
   `admin.css` dim their own Refresh buttons the same way. */
.refresh-btn:disabled {
  opacity: 0.55;
  cursor: default;
  transform: none;
}

.refresh-btn:disabled:hover { background: var(--glass); }

.refresh-icon {
  font-size: 1.05rem;
  line-height: 1;
  color: var(--accent);
}

.refresh-btn.is-busy .refresh-icon {
  animation: spin 0.7s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Empty unless a refresh failed. Two things here, both measured rather than
   guessed:

   - **Out of flow.** The message is half again as wide as the button, and as a
     grid row it widened `.refresh-box` to fit — in a flex row packed to the
     right, that shoved the search input 53px sideways the instant a refresh
     failed. An error message is the last thing that should move the page under
     you. Absolute also removes the need for a `min-height` placeholder line.
   - **Anchored to `.section-tools`, not to `.refresh-box`.** Off the button it
     lands on the same line as the search's result count and overlapped it by
     38px. The tools row is as tall as its tallest control, so hanging the
     message below *that* clears the count whatever is in it. */
.refresh-status {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  white-space: nowrap;
  font-size: 0.8rem;
  color: var(--ink-faint);
}

/* Stats
   -------------------------------------------------------------------------- */
.stat-grid {
  margin-top: 34px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 18px;
}

.stat-tile {
  /* Makes the tile the reference for its own contents' `cqi` sizing — see
     .stat-value, which has to fit inside this box rather than inside the
     window. */
  container-type: inline-size;
  background: var(--glass);
  border: 1px solid var(--glass-line);
  border-radius: 18px;
  padding: 26px 22px;
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.3);
  transition: transform 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
}

.stat-tile:hover {
  transform: translateY(-6px);
  background: rgba(255, 255, 255, 0.11);
  box-shadow: 0 22px 46px rgba(0, 0, 0, 0.45);
}

.stat-icon { font-size: 1.35rem; color: var(--accent); }

.stat-value {
  display: block;
  margin-top: 8px;
  /* Fallback for anything without container query units: flat, and small enough
     that the longest number still fits the narrowest tile the grid can make. */
  font-size: 1.5rem;
  /* The number is sized by its tile, not by the window. `5vw` scaled with the
     viewport while the tile's width came from the grid's auto-fit — unrelated
     axes, so "Total score" fit at some widths and spilled straight out of the
     card at others (197px of digits in a 170px box, and worst on a wide screen,
     where five columns make each tile *narrower*).
     100cqi is the tile's content box, so this can't drift again, and the 2.5rem
     cap keeps the original size on a single-column phone whose tile is wide
     enough for it. Measured ceiling at the tightest layout (five columns on a
     1280px screen, 152px of content): 999,999,999 fits, ten figures spill by
     15px. If the team's total score ever reaches a billion, this is the dial —
     14cqi covers it, at the cost of a smaller number everywhere today. */
  font-size: clamp(1.35rem, 16cqi, 2.5rem);
  font-weight: 800;
  /* Tabular figures also stop the count-up animation from jittering the width. */
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.stat-label {
  display: block;
  margin-top: 6px;
  font-size: 0.86rem;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* Recent Scores
   -------------------------------------------------------------------------- */
.score-table {
  margin-top: 34px;
  background: var(--glass);
  border: 1px solid var(--glass-line);
  border-radius: 18px;
  overflow: hidden;
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}

.score-row {
  position: relative;
  display: grid;
  /* auto, not a fixed px, for the figures column — .score-figures below
     fixes its own two sub-columns to constant widths, so its rendered
     width never depends on how wide this particular row's number or date
     happen to be. That's what actually keeps every row's figures aligned:
     each .score-row is its own independent grid (there's no single shared
     grid spanning the whole table), so an `auto` track only ever matches
     across rows when the thing filling it can't vary by content — a fixed
     200px here would work too, but would silently drift out of sync the
     moment .score-figures' own widths below were ever changed without it. */
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 14px;
  padding: 14px 18px;
  border-top: 1px solid rgba(255, 255, 255, 0.09);
  color: inherit;
  text-decoration: none;
  /* A tap's feedback needs to be immediate, not a fade-in after the fact —
     only `background` transitions, so :active below reads as instant. */
  transition: background 0.15s ease;
}

.score-row:first-child { border-top: none; }
a.score-row:hover { background: rgba(255, 255, 255, 0.05); }

/* Touch has no hover — without this, tapping a row on mobile gives no
   feedback at all until the profile page has already started loading. */
a.score-row:active { background: rgba(255, 255, 255, 0.1); }

.score-head {
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-faint);
  background: rgba(255, 255, 255, 0.05);
}

/* A row wears its player's equipped banner_bg — see getRecentScores in
   module/homeData.js. Most players have nothing equipped, so this is
   opt-in via .has-art the same way .post-card/.player-card only pay for
   the extra layer and scrim when there's art to show. */
.score-row-art {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Tinted with the same dark stop .post-card's own scrim defaults to
   (body's first gradient stop, #0f2027) — this is the banner alone, not a
   themed row. Left-to-right, not top-to-bottom like .post-card's: a row is
   wide and short rather than tall, so a vertical fade would only ever
   darken a thin strip at the very top or bottom of it. */
.score-row.has-art::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    90deg,
    rgba(15, 32, 39, 0.9) 0%,
    rgba(15, 32, 39, 0.55) 55%,
    rgba(15, 32, 39, 0.2) 100%
  );
}

.score-row.has-art .score-player,
.score-row.has-art .score-figures {
  position: relative;
  z-index: 2;
}

.score-row.has-art .score-name { text-shadow: 0 2px 8px rgba(0, 0, 0, 0.75); }

.score-player {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.score-avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  object-fit: cover;
  flex: none;
  background: rgba(255, 255, 255, 0.1);
  box-shadow: 0 0 0 2px rgba(var(--accent-rgb), 0.4);
}

.score-name {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

a.score-row:hover .score-name { color: var(--accent); }

/* Fixed-width sub-columns, not a flex `space-between` — a flex box sizes
   itself to its own content, so the whole figures block (and therefore
   .score-row's own `auto` track above) would be a different width on a
   6-figure score than a 7-figure one, and the header's short "Score"/"Date"
   labels would size differently again. Two constant-width grid tracks make
   .score-figures the same total width on every row regardless of what's in
   it, which is what actually keeps the value's right edge lined up all the
   way down the table. */
.score-figures {
  display: grid;
  grid-template-columns: 84px 90px;
  column-gap: 12px;
  align-items: center;
}

.score-value {
  font-weight: 800;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
  text-align: right;
}

.score-head .score-value,
.score-head .score-date { color: inherit; font-weight: inherit; }

.score-date {
  color: var(--ink-dim);
  font-size: 0.86rem;
  white-space: nowrap;
  text-align: right;
}

/* Narrower and denser rather than restacked into two lines — an earlier
   version dropped to a single grid column here, splitting every row across
   two lines of uneven height (the header's own two labels included). Keeping
   the same two-column shape, just tighter, is what keeps every row one
   smooth, evenly tall line down to a narrow phone — and .score-figures'
   sub-columns staying fixed-width (just narrower) is what keeps the score
   aligned at this size too, not just on desktop. */
@media (max-width: 560px) {
  .score-row {
    grid-template-columns: 1fr auto;
    padding: 12px 14px;
    gap: 10px;
  }

  .score-avatar { width: 30px; height: 30px; }
  .score-name { font-size: 0.88rem; }

  .score-figures { grid-template-columns: 58px 66px; column-gap: 8px; }
  .score-value { font-size: 0.9rem; }
  .score-date { font-size: 0.7rem; }
}

/* Memories rail
   -------------------------------------------------------------------------- */
.rail-controls { display: flex; gap: 10px; }

.rail-btn {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  font-size: 1.5rem;
  line-height: 1;
  color: var(--ink);
  cursor: pointer;
  background: var(--glass);
  border: 1px solid var(--glass-line);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  transition: background 0.2s ease, transform 0.15s ease, opacity 0.2s ease;
}

.rail-btn:hover { background: rgba(255, 255, 255, 0.16); }
.rail-btn:active { transform: scale(0.94); }
.rail-btn:disabled { opacity: 0.35; cursor: default; }

/* The one horizontally scrolling element on the page. Snapping makes the drag
   land on a card instead of between two.
   `proximity`, not `mandatory`: mandatory snapping pinned this rail at
   scrollLeft 0 and re-snapped every programmatic scroll straight back, which
   left the arrow buttons dead. Observed, not theorised — don't tighten it. */
.rail {
  margin-top: 30px;
  display: flex;
  gap: 20px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-behavior: smooth;
  padding: 6px 4px 26px;
  /* Room for the hover lift, which would otherwise be clipped by overflow. */
  scrollbar-width: thin;
  scrollbar-color: rgba(var(--accent-rgb), 0.5) transparent;
}

.rail::-webkit-scrollbar { height: 8px; }
.rail::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.06); border-radius: 99px; }
.rail::-webkit-scrollbar-thumb {
  background: rgba(var(--accent-rgb), 0.45);
  border-radius: 99px;
}

.rail:focus-visible { outline: 2px solid rgba(var(--accent-rgb), 0.6); outline-offset: 6px; }

.memory {
  flex: 0 0 clamp(250px, 62vw, 400px);
  scroll-snap-align: start;
  border-radius: 18px;
  overflow: hidden;
  position: relative;
  background: var(--glass);
  border: 1px solid var(--glass-line);
  box-shadow: 0 16px 38px rgba(0, 0, 0, 0.4);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.memory:hover { transform: translateY(-6px); box-shadow: 0 26px 52px rgba(0, 0, 0, 0.55); }

.memory img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  /* Slight zoom-out on hover reads as the photo settling. */
  transform: scale(1.02);
  transition: transform 0.5s ease;
}

.memory:hover img { transform: scale(1.07); }

.memory figcaption {
  position: absolute;
  inset: auto 0 0 0;
  padding: 30px 16px 14px;
  font-size: 0.92rem;
  font-weight: 600;
  background: linear-gradient(transparent, rgba(4, 16, 22, 0.92));
}

/* The click target that opens #memoryLightbox — wraps the same <img> the
   plain `.memory img` rule above already styles (a descendant selector, so
   the extra nesting changes nothing there), reset to plain block so a
   <button>'s own border/background/padding don't show through under it.
   figcaption stays a sibling positioned over the whole figure, same as
   before this button existed. */
.memory-media {
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}

.memory-media:focus-visible {
  outline: 2px solid rgba(var(--accent-rgb), 0.6);
  outline-offset: -2px;
}

/* Recent Posts
   --------------------------------------------------------------------------
   Two independent grids — Gameplay Clips and Captures — each a small "what's
   new" strip rather than a full listing (see RECENT_POSTS_LIMIT in
   module/homeData.js). Cards share the glass-tile language the rest of the
   page uses (.stat-tile, .memory, .player-card) rather than inventing a
   fourth look. */
.post-group { margin-top: 34px; }

.post-group + .post-group { margin-top: 44px; }

.post-group-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
}

.post-group-title {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--ink-dim);
  letter-spacing: 0.4px;
}

/* Links to /posts?type=video|capture — the full, paginated listing this
   teaser's fixed six are drawn from. */
.post-group-link {
  flex: none;
  color: var(--accent);
  font-size: 0.86rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

.post-group-link:hover { text-decoration: underline; }

.post-grid {
  margin-top: 18px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 18px;
}

.post-card {
  position: relative;
  display: grid;
  border-radius: 16px;
  overflow: hidden;
  background: var(--glass);
  border: 1px solid var(--glass-line);
  box-shadow: 0 14px 32px rgba(0, 0, 0, 0.32);
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}

.post-card:hover { transform: translateY(-5px); box-shadow: 0 22px 44px rgba(0, 0, 0, 0.45); }

/* A post wears its poster's equipped profile_bg behind the card — see
   postArtNode in home.js (duplicated in posts.js). Most posts have nothing
   equipped, so this is opt-in via .has-art rather than every card paying for
   the extra layer and scrim, the same restraint the roster tiles below take.
   (.post-card itself already carries `position: relative` unconditionally,
   for .post-share's own absolute positioning below, so there is nothing
   left for .has-art to add here — unlike .player-card.has-art, which is the
   only thing on that tile ever needing it.) */
.post-card-art {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* .post-media is already opaque (a video/image fills it edge to edge), so in
   practice this scrim only ever shows through behind .post-meta — but one
   rectangle covering the whole card is simpler than two matching the visible
   region exactly. Tinted with the same dark stop .player-card's own scrim
   defaults to (body's first gradient stop, #0f2027), not the poster's palette
   — this is the artwork alone, not a themed card. */
.post-card.has-art::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    180deg,
    rgba(15, 32, 39, 0.2) 0%,
    rgba(15, 32, 39, 0.55) 55%,
    rgba(15, 32, 39, 0.9) 100%
  );
}

/* Lifts the real content above the artwork and its scrim. */
.post-card.has-art .post-media,
.post-card.has-art .post-meta {
  position: relative;
  z-index: 2;
}

/* The thumbnail is a <button>, not an <img>/<video> directly in the card, so
   the whole preview area is one click target for the lightbox rather than
   requiring a precise hit on the media element itself. */
.post-media {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  padding: 0;
  border: none;
  background: rgba(0, 0, 0, 0.35);
  cursor: pointer;
}

.post-thumb {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Videos ship muted with no native controls in the thumbnail (see postThumb
   in home.js) — this glyph is what says "this opens bigger", the same job
   the scroll-cue plays in the hero. */
.post-play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 1.6rem;
  color: #fff;
  background: rgba(4, 16, 22, 0.28);
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.6);
  transition: background 0.2s ease;
}

.post-media:hover .post-play { background: rgba(4, 16, 22, 0.42); }

.post-meta {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 14px 16px 16px;
}

.post-avatar-link { flex: none; }

.post-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  object-fit: cover;
  background: rgba(255, 255, 255, 0.1);
  box-shadow: 0 0 0 2px rgba(var(--accent-rgb), 0.4);
}

.post-info { min-width: 0; }

.post-username {
  display: block;
  color: var(--ink);
  font-weight: 700;
  font-size: 0.92rem;
  text-decoration: none;
  overflow-wrap: anywhere;
}

.post-username:hover { color: var(--accent); }

.post-title {
  margin-top: 3px;
  color: var(--ink-dim);
  font-size: 0.86rem;
  line-height: 1.4;
  overflow-wrap: break-word;
}

/* Copies a link straight to this post on /player (see sharePost in home.js,
   duplicated in posts.js). Pinned to the card's own top-right corner with
   `position: absolute` rather than flowing at the end of .post-meta — a flex
   position there drifts sideways (or wraps to its own line) once a long
   nickname or title pushes it around, where an absolute offset from the
   card never moves regardless of what .post-meta holds. That also means it
   now sits over the thumbnail rather than beside the avatar, so the glass
   look .post-meta's own controls use would wash out against arbitrary video
   or image content underneath it — this is a solid, higher-contrast badge
   instead, the same reasoning .post-play's scrim follows. z-index 3 clears
   .post-media/.post-meta's own 2 (see .post-card.has-art above) whether or
   not this particular card has art. */
.post-share {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 3;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.25);
  background: rgba(4, 16, 22, 0.55);
  color: #fff;
  font-size: 0.92rem;
  line-height: 1;
  cursor: pointer;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
  transition: background 0.2s ease, transform 0.15s ease, border-color 0.2s ease;
}

.post-share:hover { background: rgba(4, 16, 22, 0.8); }

.post-share:active { transform: scale(0.94); }

.post-share:disabled { cursor: default; }

/* The button's own glyph is the "copied" feedback (see flashShareButton) —
   this just tints the badge while that's showing, success or failure alike
   get the accent since both are informational rather than an error state. */
.post-share.is-copied {
  background: rgba(4, 16, 22, 0.8);
  border-color: rgba(var(--accent-rgb), 0.6);
  color: var(--accent);
}

/* Marks a card in the WOW Kills section — see wowKillCard in home.js
   (duplicated in posts.js). Pinned top-left so it never collides with
   .post-share's own absolute top-right corner; z-index 3 for the same
   "clear .has-art's scrim either way" reason .post-share needs it. A solid
   warm pill rather than the glass look .post-share uses, so it reads as a
   badge rather than a control. */
.post-wow-badge {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 3;
  padding: 4px 10px;
  border-radius: 999px;
  background: linear-gradient(135deg, #ff9a3c, #ff5f6d);
  color: #1a0a02;
  font-size: 0.74rem;
  font-weight: 800;
  letter-spacing: 0.02em;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
}

/* The lightbox — a clip or capture at full size, opened by clicking a card's
   .post-media. Two-class .hidden pairing for the same reason player.css's
   .modal-overlay.hidden is: this element sets its own display to centre the
   card, and a bare single-class .hidden from further down this file would tie
   with it on specificity and could lose depending on load order. */
.post-lightbox.hidden { display: none; }

.post-lightbox {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.post-lightbox-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(4, 16, 22, 0.82);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.post-lightbox-card {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: min(900px, 92vw);
  max-height: 90vh;
  display: grid;
  border-radius: 18px;
  overflow: hidden;
  background: #0c1c24;
  border: 1px solid var(--glass-line);
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.6);
}

.post-lightbox-close {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 2;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  border: 1px solid var(--glass-line);
  background: rgba(4, 16, 22, 0.6);
  color: var(--ink);
  font-size: 1rem;
  cursor: pointer;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  transition: background 0.2s ease;
}

.post-lightbox-close:hover { background: rgba(4, 16, 22, 0.85); }

/* Prev/next lightbox nav — shared by /posts' own lightbox (posts.js, loaded
   on top of this stylesheet) and the Memories lightbox below, both of which
   step through an already-known list. Not used by this page's own Recent
   Posts teaser lightbox: that grid is a fixed six-item preview with nothing
   to step through. Centred on the whole card rather than measuring
   .post-lightbox-media alone — the caption strip below the media is short
   enough that the offset from true media-centre is negligible, and this
   avoids a JS measurement for every image/video aspect ratio. Same
   glass-circle treatment as .post-lightbox-close above so all three read as
   one control family. */
.post-lightbox-nav {
  position: absolute;
  top: 50%;
  z-index: 2;
  transform: translateY(-50%);
  width: 42px;
  height: 42px;
  border-radius: 50%;
  border: 1px solid var(--glass-line);
  background: rgba(4, 16, 22, 0.6);
  color: var(--ink);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  transition: background 0.2s ease, opacity 0.2s ease;
}

.post-lightbox-nav:hover:not(:disabled) { background: rgba(4, 16, 22, 0.85); }

/* Dimmed rather than removed: at either end of the list the reader should
   see there's nothing further rather than the arrow silently vanishing and
   the layout shifting. Matches how home.js/player.js/admin.js already dim
   their own Refresh buttons while disabled. */
.post-lightbox-nav:disabled {
  opacity: 0.3;
  cursor: default;
}

.post-lightbox-prev { left: 12px; }
.post-lightbox-next { right: 12px; }

@media (max-width: 480px) {
  .post-lightbox-nav { width: 36px; height: 36px; font-size: 1.2rem; }
}

.post-lightbox-media {
  display: grid;
  place-items: center;
  max-height: 74vh;
  background: #000;
}

.post-lightbox-video,
.post-lightbox-image {
  display: block;
  width: 100%;
  max-height: 74vh;
  object-fit: contain;
}

.post-lightbox-caption {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 18px;
  border-top: 1px solid var(--glass-line);
}

.post-lightbox-user {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--ink);
  text-decoration: none;
  font-weight: 700;
}

.post-lightbox-user:hover { color: var(--accent); }

.post-lightbox-avatar {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  object-fit: cover;
  background: rgba(255, 255, 255, 0.1);
}

.post-lightbox-title {
  margin-left: auto;
  color: var(--ink-dim);
  font-size: 0.88rem;
  text-align: right;
  overflow-wrap: break-word;
}

@media (max-width: 620px) {
  .post-lightbox-caption { flex-wrap: wrap; }
  .post-lightbox-title { margin-left: 0; text-align: left; }
}

/* #memoryLightbox's own caption — a memory has no poster to credit (unlike a
   post), so this is just the screenshot's filename-derived title, left-aligned
   and full-width rather than .post-lightbox-title's right-aligned "sits
   beside the avatar" treatment. */
.memory-lightbox-title {
  margin: 0;
  color: var(--ink);
  font-size: 0.95rem;
  font-weight: 600;
  overflow-wrap: break-word;
}

/* Stats help — the "?" beside the Stats heading and the popup it opens
   -------------------------------------------------------------------------- */
.stats-help-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  margin-left: 10px;
  vertical-align: middle;
  border-radius: 50%;
  border: 1px solid var(--glass-line);
  background: var(--glass);
  color: var(--ink-dim);
  font-size: 0.85rem;
  font-weight: 700;
  cursor: pointer;
  transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}

.stats-help-btn:hover { background: rgba(255, 255, 255, 0.14); color: var(--ink); }

.stats-help-btn:focus-visible {
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.6);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.16);
}

/* Same fixed-overlay/backdrop/card shape .post-lightbox uses above, just
   holding a plain info list instead of media. Two-class .hidden pairing for
   the reason documented at .post-lightbox.hidden: this element sets its own
   `display: flex` to centre the card, and a bare single-class `.hidden`
   would tie with it on specificity depending on stylesheet order. */
.stats-help-overlay.hidden { display: none; }

.stats-help-overlay {
  position: fixed;
  inset: 0;
  z-index: 80;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}

.stats-help-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(4, 16, 22, 0.82);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}

.stats-help-card {
  position: relative;
  z-index: 1;
  width: 100%;
  max-width: 480px;
  max-height: 86vh;
  overflow-y: auto;
  padding: 28px;
  border-radius: 18px;
  background: #0c1c24;
  border: 1px solid var(--glass-line);
  box-shadow: 0 30px 70px rgba(0, 0, 0, 0.6);
}

.stats-help-close {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 1px solid var(--glass-line);
  background: rgba(4, 16, 22, 0.6);
  color: var(--ink);
  font-size: 0.95rem;
  cursor: pointer;
  transition: background 0.2s ease;
}

.stats-help-close:hover { background: rgba(4, 16, 22, 0.85); }

.stats-help-heading {
  margin: 0 34px 6px 0;
  font-size: 1.25rem;
  font-weight: 800;
}

.stats-help-intro {
  margin: 0 0 20px;
  color: var(--ink-faint);
  font-size: 0.88rem;
  line-height: 1.5;
}

.stats-help-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.stats-help-item {
  display: flex;
  align-items: flex-start;
  gap: 14px;
}

.stats-help-icon {
  flex: none;
  width: 30px;
  font-size: 1.25rem;
  line-height: 1.5;
  text-align: center;
}

.stats-help-item > div {
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.stats-help-rule-title {
  margin: 0;
  color: var(--ink);
  font-size: 0.94rem;
  font-weight: 700;
}

.stats-help-desc {
  margin: 0;
  color: var(--ink-dim);
  font-size: 0.92rem;
  line-height: 1.55;
}

.stats-help-item strong { color: var(--ink); }

/* Roster
   -------------------------------------------------------------------------- */
.player-grid {
  margin-top: 34px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: 18px;
}

/* Roster search — see "The roster search" in home.js. Sits at the end of the
   section head, where the Memories rail keeps its arrows. */
.player-search {
  display: grid;
  gap: 6px;
  justify-items: end;
}

.player-search-input {
  width: clamp(200px, 30vw, 280px);
  padding: 11px 16px;
  border-radius: 999px;
  font: inherit;
  font-size: 0.92rem;
  color: var(--ink);
  background: var(--glass);
  border: 1px solid var(--glass-line);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
}

.player-search-input::placeholder { color: var(--ink-faint); }

.player-search-input:focus {
  outline: none;
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(var(--accent-rgb), 0.55);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.16);
}

/* Empty until something is typed, but it always occupies its line — otherwise
   the count appearing would shift the input up on the first keystroke. */
.player-search-count {
  min-height: 1.1em;
  font-size: 0.8rem;
  color: var(--ink-faint);
  padding-right: 4px;
}

/* Hidden where they stand rather than removed, so their avatars stay loaded and
   the grid doesn't churn a node per keystroke. Carries both jobs — the tiles a
   query didn't match, and the ones past the twelve-tile cap — because one class
   set by one pass can't fall out of step with itself (see home.js). */
.player-card.is-filtered-out { display: none; }

/* Reveals the rest of the roster. The wrapper does the centring so that
   `.roster-toggle` never declares a `display` of its own and `.hidden` can put
   the button away without a specificity tie to lose. */
.roster-more {
  display: flex;
  justify-content: center;
}

.roster-toggle {
  margin-top: 30px;
  padding: 13px 28px;
  border-radius: 999px;
  font: inherit;
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--ink);
  cursor: pointer;
  background: var(--glass);
  border: 1px solid rgba(var(--accent-rgb), 0.35);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  transition: background 0.2s ease, transform 0.15s ease, border-color 0.2s ease;
}

.roster-toggle:hover {
  background: rgba(255, 255, 255, 0.16);
  border-color: rgba(var(--accent-rgb), 0.6);
}

.roster-toggle:active { transform: scale(0.97); }

.roster-toggle:focus-visible {
  outline: none;
  border-color: rgba(var(--accent-rgb), 0.7);
  box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.16);
}

/* `dropIn` is cancelled and replayed every time a tile returns from
   `display: none`, which during a search is most of the grid on every letter.
   The entrance has long finished by the time anyone can type, so the first
   query retires it. Added by home.js, never removed. */
.player-grid.is-searched .player-card { animation: none; }

/* A tile wears its player's equipped background and chosen palette when they
   have them — see "The tile's look" in home.js. Both are optional and most of
   the roster has neither, so the plain tile below is still the common case and
   is unchanged by either.

   The three colours are custom properties set inline per card, which is what
   lets the rules further down be written once instead of in themed and unthemed
   pairs. The values here are the fallbacks for a card home.js didn't touch. */
.player-card {
  --card-accent: var(--accent);
  --card-accent-rgb: var(--accent-rgb);
  --card-accent-2: var(--accent-2);
  /* Tint of the scrim over the artwork; overridden with the palette's darkest
     stop. Matches body's first gradient stop, #0f2027. */
  --card-dim-rgb: 15, 32, 39;

  /* The containing block for the artwork and the accent bar, both absolute.
     Cheap and unconditional; the clipping they also need is not — see below. */
  position: relative;
  display: grid;
  justify-items: center;
  gap: 12px;
  padding: 24px 14px 20px;
  border-radius: 18px;
  text-decoration: none;
  color: var(--ink);
  background: var(--glass);
  border: 1px solid var(--glass-line);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3);
  transition: transform 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
  /* Staggered entrance, one card after another; delay set inline by home.js. */
  animation: dropIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
}

/* Only the tiles that actually have something to clip get clipped. The grid
   draws a card per active player — 200-odd of them — and every one carries a
   backdrop-filter, which is already the expensive part of this section; adding
   a clip to a backdrop-filtered element makes it its own composited layer. Both
   variants below are a small minority of the roster, and a plain tile is left
   exactly as it was. */
.player-card.has-art,
.player-card.is-themed {
  overflow: hidden;
}

/* Lifts the content over the artwork and its scrim. Only needed where there is
   artwork — the accent bar sits on the top edge and overlaps neither. */
.player-card.has-art > .player-avatar,
.player-card.has-art > .player-name {
  position: relative;
  z-index: 2;
}

.player-card:hover {
  transform: translateY(-7px);
  background: rgba(255, 255, 255, 0.13);
  box-shadow: 0 24px 48px rgba(0, 0, 0, 0.5);
}

/* Equipped profile background
   -------------------------------------------------------------------------- */
.player-card-art {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Slightly overscanned so the lift on hover can't expose an edge. */
  transform: scale(1.04);
  transition: transform 0.4s ease;
}

.player-card.has-art:hover .player-card-art { transform: scale(1.1); }

/* A three.js scene mounted by animatedBg.js in place of the <img> above, for
   a tile whose profile_bg is an animated preset (see animatedArtNode in
   home.js) — duplicated from shop.css's identical rule rather than loading
   that whole stylesheet here, the same "classic scripts, no module system"
   duplication this file's own hexToRgb/squash already follow. overflow:
   hidden isn't needed here — .player-card.has-art already clips via its own
   overflow: hidden — so this only needs to size and lay out the canvas
   itself; pointer-events: none keeps it from stealing the tile's own click.
   .player-card-art already gives .animated-bg-host its position: absolute
   and the hover-zoom transform above, so nothing else is added to that
   selector here. */
.animated-bg-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

/* The artwork covers the whole tile, so the blur behind it can never be seen —
   and a backdrop-filter per tile is the expensive part of a 200-tile grid. */
.player-card.has-art {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* Holds the nickname legible over artwork nobody vetted for contrast: heaviest
   at the foot of the tile, where the text is. Tinted with the palette's darkest
   stop so the dimming reads as part of the theme rather than flat black. */
.player-card.has-art::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    180deg,
    rgba(var(--card-dim-rgb), 0.34) 0%,
    rgba(var(--card-dim-rgb), 0.7) 55%,
    rgba(var(--card-dim-rgb), 0.92) 100%
  );
}

.player-card.has-art .player-name { text-shadow: 0 2px 10px rgba(0, 0, 0, 0.75); }

/* Chosen theme
   --------------------------------------------------------------------------
   Opt-in via the class: without it a tile keeps the neutral border and ring it
   has always had, rather than every card on the page picking up the default
   accent and the section losing the contrast that makes a theme mean anything. */
.player-card.is-themed {
  border-color: rgba(var(--card-accent-rgb), 0.42);
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3), inset 0 0 22px rgba(var(--card-accent-rgb), 0.08);
}

.player-card.is-themed:hover {
  box-shadow: 0 24px 48px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(var(--card-accent-rgb), 0.55);
}

/* The palette itself, as a bar across the top. The name stays white on purpose —
   thirteen palettes against artwork nobody vetted is too many ways for coloured
   text to end up unreadable, and this shows both accents anyway. */
.player-card.is-themed::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  z-index: 3;
  height: 3px;
  background: linear-gradient(90deg, var(--card-accent), var(--card-accent-2));
}

.player-avatar {
  width: 86px;
  height: 86px;
  border-radius: 50%;
  object-fit: cover;
  background: rgba(255, 255, 255, 0.1);
  box-shadow: 0 0 0 2px rgba(var(--accent-rgb), 0.45), 0 10px 24px rgba(0, 0, 0, 0.4);
}

/* Stand-in when players.avatar is empty — the team logo.
   `contain` rather than the inherited `cover`, with padding to hold the mark off
   the ring: the logo is a square badge, and cover would crop its corners against
   a circle. The flatter background is what separates a placeholder from a real
   photo at a glance. */
.player-avatar-logo {
  object-fit: contain;
  padding: 13px;
  background: rgba(255, 255, 255, 0.05);
}

/* The ring in the player's own accent. Below the two rules it overrides purely
   so the avatar is defined before it is themed — it wins on specificity, not
   order. Applies to the logo stand-in too: the ring says whose tile this is,
   which is true whether or not there is a photo inside it. */
.player-card.is-themed .player-avatar {
  box-shadow: 0 0 0 2px rgba(var(--card-accent-rgb), 0.75), 0 10px 24px rgba(0, 0, 0, 0.4);
}

.player-name {
  font-weight: 700;
  font-size: 0.98rem;
  text-align: center;
  overflow-wrap: anywhere;
}

/* Footer
   -------------------------------------------------------------------------- */
.footer {
  display: grid;
  justify-items: center;
  gap: 12px;
  padding: 50px 20px 60px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(4, 16, 22, 0.4);
}

.footer img { border-radius: 10px; opacity: 0.9; }

.footer-slogan {
  letter-spacing: 2px;
  font-size: 0.9rem;
  color: var(--ink-dim);
  text-align: center;
}

.footer-links { display: flex; gap: 20px; flex-wrap: wrap; justify-content: center; }

.footer-links a {
  color: var(--ink-faint);
  text-decoration: none;
  font-size: 0.88rem;
  transition: color 0.2s ease;
}

.footer-links a:hover { color: var(--accent); }

.hidden { display: none; }

@media (max-width: 620px) {
  html { scroll-padding-top: 76px; }
  .topnav { gap: 10px; }
  .topnav a { font-size: 0.85rem; padding: 6px 2px; }
  .brand span { display: none; }
  .section-head { align-items: flex-start; }
  /* The head has wrapped by now, so the tools take the whole second row and
     stack against the left edge rather than sitting in a short right-aligned
     column. The search below is full-width, which is what wraps the refresh
     button onto a line of its own. */
  .section-tools { width: 100%; justify-content: flex-start; }
  .player-search { width: 100%; justify-items: stretch; }
  .player-search-input { width: 100%; }
  .player-search-count { padding-right: 0; }
  /* Same wrap, but the button keeps its own width — a full-bleed pill would
     read as the section's primary action, which it isn't. The status follows
     the button to the left edge. */
  .refresh-box { width: 100%; justify-items: start; }
  .refresh-status { left: 0; right: auto; }
  /* The account circle drops to 34px here too; that rule is in authslot.css,
     since it is the same control on every page. */
}

/* Phone widths — 375, 390 and 414 all land here. The topbar is full at four
   items plus the account circle, so the last of the slack comes out of the gaps
   and the type scale. */
@media (max-width: 430px) {
  /* `.topnav` is a nowrap flex row whose items shrink, so overshooting the width
     doesn't overflow — it squeezes "Join us" onto two lines and makes the whole
     bar 18px taller, which then throws off scroll-padding-top. The nowrap below
     turns that silent reflow into visible overflow, so a future near-miss is
     obvious rather than mysterious.

     Measured slack is 21px at 375px wide and 6px at 360px, and — unlike the text
     pill this replaced — it is now the *same* at both auth states, because the
     circle is one fixed width. There is still no room for a fifth item. */
  .topnav { gap: 6px; }
  .topnav a { font-size: 0.82rem; }
  .nav-cta { padding: 8px 14px !important; white-space: nowrap; }
}

/* Everything above animates; none of it is load-bearing, so it all comes off
   for a visitor who asked for less motion — including the smooth scrolling,
   which is the one piece home.js can't opt out of on its own. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .rail { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation: none !important;
    transition-duration: 0.01ms !important;
  }

  .reveal { opacity: 1; transform: none; }

  /* The blanket rule above kills the spin, so fetching and cooling look alike
     here — both just dimmed by `.refresh-btn:disabled`, which is feedback
     enough that the button is unavailable. Their titles still differ. */
}
