

:root {
  /* One knob for the ladder's overall scale; the countdown divides it out. */
  --ladder-zoom: 1.2;

  --ease: cubic-bezier(0.32, 0.72, 0, 1);
  --lp-up: #5bd6a0;
  --lp-down: #e0637f;
  --stat-accent: #7fc4ff;
  --crown-gold: #f0c250;
  --crown: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 18'%3E%3Cpath d='M1 17V3l6 5 6-8 6 8 6-5v14z' fill='%23fff'/%3E%3C/svg%3E");
  --hairline: #1e2430;
  /* Every figure on the page speaks in one mono voice, tabular by default. */
  --numeric: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace;
  --font-display: "Space Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;

  
  --tier-iron: #8f8f8f;
  --tier-bronze: #c08551;
  --tier-silver: #9aa7b4;
  --tier-gold: #d0a248;
  --tier-platinum: #4bb3a5;
  --tier-emerald: #46b06a;
  --tier-diamond: #6b8fe0;
  --tier-master: #b06ad0;
  --tier-grandmaster: #d05a5a;
  --tier-challenger: #d8b45a;
}

body {
  font-family: var(--font-display);
}

/* The page shell (flex column, min-height: 100dvh, a growing main) is the
   shared one in style.css now; short ladder states compose like every page. */




.ladder-masthead {
  position: relative;
  padding: clamp(40px, 6vw, 84px) 0 clamp(28px, 4vw, 52px);
  text-align: center;
}

/*
 * The page's namesake, drawn once and quietly: ladder rungs behind the clock,
 * fading out radially so they read as depth rather than as stripes.
 */
.ladder-masthead::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: clamp(18px, 3vw, 40px);
  width: min(720px, 86vw);
  height: clamp(96px, 13vw, 150px);
  transform: translateX(-50%);
  background: repeating-linear-gradient(
    180deg,
    var(--hairline) 0 1px,
    transparent 1px 24px
  );
  -webkit-mask-image: radial-gradient(52% 88% at 50% 46%, #000 30%, transparent 78%);
          mask-image: radial-gradient(52% 88% at 50% 46%, #000 30%, transparent 78%);
  pointer-events: none;
}

.masthead-kicker,
.masthead-clock {
  position: relative;
  z-index: 1;
}

.masthead-kicker {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  margin: 0 0 clamp(14px, 2vw, 24px);
}

/* The countdown label; the only thing left in the kicker. */
.masthead-kicker > span:last-child {
  color: var(--muted);
  font-family: var(--numeric);
  font-size: 0.66rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.masthead-clock {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(14px, 2.4vw, 30px);
}

.clock-segment {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.clock-value {
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  
  font-size: clamp(2.7rem, 7vw, 5.4rem);
  font-weight: 400;
  line-height: 0.94;
  letter-spacing: -0.04em;
  color: var(--text);
}


.clock-value.is-ticking {
  animation: clock-roll 480ms var(--ease);
}

@keyframes clock-roll {
  from {
    transform: translateY(0.28em);
    opacity: 0;
  }
}

.clock-unit {
  font-size: 0.64rem;
  text-transform: uppercase;
  letter-spacing: 0.22em;
  color: var(--muted);
}

.clock-sep {
  align-self: center;
  font-family: var(--numeric);
  font-size: clamp(1.5rem, 3.8vw, 2.8rem);
  font-weight: 400;
  color: color-mix(in srgb, var(--text) 22%, transparent);
}

/*
 * The pre-announcement board: nobody on it yet. Same voice as #status, but
 * it lives inside the table so the countdown and prize stay above it.
 */
.ladder-empty {
  padding: clamp(48px, 8vh, 80px) 24px;
  text-align: center;
  color: var(--muted);
  font-family: var(--numeric);
  letter-spacing: 0.08em;
}

.ladder-empty-title {
  margin: 0 0 10px;
  color: var(--text);
  font-size: 0.88rem;
  font-weight: 700;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

.ladder-empty p:not(.ladder-empty-title) {
  margin: 0 auto;
  max-width: 520px;
  font-size: 0.78rem;
  line-height: 1.7;
  color: var(--muted);
}

/* Signup form — lives inside the empty-state note. */
.signup-form {
  margin: 26px auto 0;
  max-width: 420px;
}

.signup-label {
  display: block;
  margin-bottom: 12px;
  color: var(--text);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.signup-row {
  display: flex;
  gap: 10px;
}

.signup-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 10px 14px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  font-family: var(--numeric);
  font-size: 0.9rem;
  letter-spacing: 0.02em;
}

.signup-input:focus {
  outline: none;
  border-color: #d8ba6b;
  box-shadow: 0 0 0 1px rgba(216, 186, 107, 0.35);
}

.signup-input::placeholder {
  color: var(--muted);
  opacity: 0.7;
}

.signup-submit {
  flex: 0 0 auto;
  padding: 10px 22px;
  border: 1px solid #d8ba6b;
  border-radius: 8px;
  background: rgba(216, 186, 107, 0.1);
  color: #d8ba6b;
  font-family: var(--font-display);
  font-size: 0.88rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  cursor: pointer;
  transition: background 200ms ease, color 200ms ease;
}

.signup-submit:hover:not(:disabled) {
  background: #d8ba6b;
  color: #171207;
}

.signup-submit:disabled {
  opacity: 0.55;
  cursor: wait;
}

.signup-consent {
  margin: 12px 0 0;
  text-align: left;
}

.signup-consent__label {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: var(--text);
  font-size: 0.82rem;
  cursor: pointer;
  user-select: none;
}

.signup-consent__checkbox {
  accent-color: #d8ba6b;
  width: 15px;
  height: 15px;
  cursor: pointer;
}

.signup-rules-link {
  background: none;
  border: none;
  padding: 0;
  color: #d8ba6b;
  font: inherit;
  font-size: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.signup-rules-link:hover {
  color: var(--text);
}

.signup-error {
  margin: 10px 0 0;
  color: #e0637f;
  font-size: 0.78rem;
}

.signup-note {
  margin: 26px 0 0;
  color: var(--text);
  font-size: 0.82rem;
}

.signup-note b {
  color: #d8ba6b;
}

/* Registration surface container — sits above the ladder standings during registration */
.registration-surface {
  margin: 0 auto 24px;
  max-width: 520px;
}

/*
 * One-time decision notice for a settled viewer (accepted or disqualified),
 * which js/ladder/page.js fills with the existing decision card. Sits where
 * the registration card sits and shares its width discipline; the card
 * inside carries the .reg-confirmed-card styling.
 */
.registration-decision-notice {
  margin: 0 auto 24px;
  max-width: 520px;
}

.registration-surface.hidden,
.registration-surface[hidden] {
  display: none !important;
}

.registration-surface .gate,
.registration-surface .signup-gate,
.registration-surface .signup-wrapper {
  margin-top: 0;
}

/* State 3: Confirmed Registered Quiet Banner */
.reg-confirmed-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 18px;
  border-radius: 10px;
  border: 1px solid rgba(91, 214, 160, 0.35);
  background: rgba(91, 214, 160, 0.06);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.25);
  font-size: 0.84rem;
}

.reg-confirmed-card__left {
  display: flex;
  align-items: center;
  gap: 12px;
}

.reg-confirmed-card__icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: rgba(91, 214, 160, 0.18);
  border: 1px solid rgba(91, 214, 160, 0.4);
  color: var(--lp-up, #5bd6a0);
  font-weight: 700;
  font-size: 0.84rem;
  flex-shrink: 0;
}

/*
 * Status tones, one per lifecycle status the badge names. Colour is a second
 * reading of the badge text, never the only signal, but it must not lie:
 * pending waits in the site's gold (--crown-gold family), accepted keeps the
 * confirming green (the unmodified card above), and a refused decision —
 * rejected or disqualified — wears the page's own negative red (--lp-down,
 * the same family the status strip and the loss columns speak in), so no
 * second red enters the palette.
 */
.reg-confirmed-card.is-pending {
  border-color: var(--gold-line, rgba(240, 194, 80, 0.45));
  background: var(--gold-soft, rgba(240, 194, 80, 0.08));
}

.reg-confirmed-card.is-pending .reg-confirmed-card__icon {
  background: var(--gold-dim, rgba(240, 194, 80, 0.22));
  border-color: var(--gold-line, rgba(240, 194, 80, 0.45));
  color: var(--crown-gold, #f0c250);
}

.reg-confirmed-card.is-pending .reg-confirmed-card__text strong {
  color: var(--crown-gold, #f0c250);
}

.reg-confirmed-card.is-pending .reg-confirmed-badge {
  background: var(--gold-dim, rgba(240, 194, 80, 0.22));
  border-color: var(--gold-line, rgba(240, 194, 80, 0.45));
  color: var(--crown-gold, #f0c250);
}

.reg-confirmed-card.is-negative {
  border-color: color-mix(in srgb, var(--lp-down, #e0637f) 42%, transparent);
  background: color-mix(in srgb, var(--lp-down, #e0637f) 7%, transparent);
}

/* A refused decision keeps the circle, loses the green: rejected and
   disqualified share this so the icon cannot vouch for the badge beside it. */
.reg-confirmed-card.is-negative .reg-confirmed-card__icon,
.reg-confirmed-card__icon.is-negative {
  background: color-mix(in srgb, var(--lp-down, #e0637f) 14%, transparent);
  border-color: color-mix(in srgb, var(--lp-down, #e0637f) 40%, transparent);
  color: var(--lp-down, #e0637f);
}

.reg-confirmed-card.is-negative .reg-confirmed-card__text strong {
  color: var(--lp-down, #e0637f);
}

.reg-confirmed-card.is-negative .reg-confirmed-badge {
  background: color-mix(in srgb, var(--lp-down, #e0637f) 14%, transparent);
  border-color: color-mix(in srgb, var(--lp-down, #e0637f) 38%, transparent);
  color: var(--lp-down, #e0637f);
}

.reg-confirmed-card__text {
  color: #fff;
  font-family: var(--font-display, inherit);
  line-height: 1.35;
}

.reg-confirmed-card__text strong {
  color: var(--lp-up, #5bd6a0);
  font-weight: 600;
}

.reg-confirmed-card__sub {
  color: var(--muted, #8b93a3);
  font-size: 0.74rem;
  margin-top: 2px;
}

.reg-confirmed-card__meta {
  display: flex;
  align-items: center;
  gap: 8px;
}

.reg-confirmed-badge {
  display: inline-block;
  padding: 3px 8px;
  border-radius: 4px;
  font-family: var(--numeric, monospace);
  font-size: 0.68rem;
  font-weight: 700;
  background: rgba(91, 214, 160, 0.16);
  color: var(--lp-up, #5bd6a0);
  border: 1px solid rgba(91, 214, 160, 0.35);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

/* Session chrome & notices */
.signup-gate__sub {
  margin: 16px 0 0;
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 0.76rem;
  color: var(--muted);
  line-height: 1.5;
  letter-spacing: 0.01em;
}

.signup-gate__notice {
  margin: 14px 0 0;
  font-size: 0.84rem;
  color: #e0637f;
  line-height: 1.4;
}

.signup-user-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  max-width: 420px;
  margin: 0 auto 16px;
  padding: 8px 14px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.8rem;
}

.signup-user-name {
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.signup-user-name strong {
  color: var(--text);
  font-weight: 600;
}

.signup-signout-btn {
  background: none;
  border: none;
  color: #e0637f;
  cursor: pointer;
  font-size: 0.76rem;
  font-family: inherit;
  padding: 2px 6px;
  border-radius: 4px;
  transition: color 150ms ease, background 150ms ease;
  white-space: nowrap;
}

.signup-signout-btn:hover {
  color: #ff859d;
  background: rgba(224, 99, 127, 0.1);
}

.signup-auth-notice {
  max-width: 420px;
  margin: 0 auto 16px;
  padding: 10px 14px;
  border-radius: 8px;
  font-size: 0.8rem;
  line-height: 1.4;
  text-align: center;
}

.signup-auth-notice--warning {
  background: rgba(224, 99, 127, 0.12);
  border: 1px solid rgba(224, 99, 127, 0.35);
  color: #f7aab7;
}

/* Admin dashboard — the pending queue behind ?admin. */
.admin-panel {
  max-width: 640px;
  margin: 0 auto;
  padding: clamp(24px, 4vw, 40px);
}

.admin-title {
  margin: 0 0 6px;
  color: var(--text);
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.admin-hint {
  margin: 0 0 20px;
  color: var(--muted);
  font-size: 0.78rem;
}

.admin-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.admin-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 12px 16px;
  margin-bottom: 8px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.03);
}

.admin-name {
  color: var(--text);
  font-family: var(--numeric);
  font-size: 0.9rem;
}

.admin-actions {
  display: flex;
  gap: 8px;
}

.admin-accept,
.admin-reject {
  padding: 6px 14px;
  border-radius: 6px;
  border: 1px solid transparent;
  font-family: var(--font-display);
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 180ms ease, color 180ms ease;
}

.admin-accept {
  border-color: rgba(91, 214, 160, 0.5);
  background: rgba(91, 214, 160, 0.08);
  color: var(--lp-up, #5bd6a0);
}

.admin-accept:hover:not(:disabled) {
  background: #5bd6a0;
  color: #0b0e14;
}

.admin-reject {
  border-color: rgba(224, 99, 127, 0.5);
  background: rgba(224, 99, 127, 0.08);
  color: var(--lp-down, #e0637f);
}

.admin-reject:hover:not(:disabled) {
  background: #e0637f;
  color: #0b0e14;
}

.admin-accept:disabled,
.admin-reject:disabled {
  opacity: 0.5;
  cursor: wait;
}

.admin-empty {
  padding: 18px;
  color: var(--muted);
  font-family: var(--numeric);
  font-size: 0.8rem;
  text-align: center;
}

/* Status page (?status) — health checks plus the event feed. */
.status-page {
  max-width: 640px;
  margin: 0 auto;
  padding: clamp(24px, 4vw, 40px);
}

.status-title {
  margin: 0 0 10px;
  color: var(--text);
  font-family: var(--font-display);
  font-size: 1.3rem;
  font-weight: 600;
}

.status-overall {
  display: inline-block;
  margin-bottom: 18px;
  padding: 5px 16px;
  border-radius: 999px;
  font-family: var(--numeric);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.status--ok { color: var(--lp-up, #5bd6a0); background: rgba(91, 214, 160, 0.08); box-shadow: inset 0 0 0 1px rgba(91, 214, 160, 0.4); }
.status--warn { color: #e0c477; background: rgba(224, 196, 119, 0.08); box-shadow: inset 0 0 0 1px rgba(224, 196, 119, 0.4); }
.status--down { color: var(--lp-down, #e0637f); background: rgba(224, 99, 127, 0.08); box-shadow: inset 0 0 0 1px rgba(224, 99, 127, 0.4); }
.status--idle { color: var(--muted); background: rgba(255, 255, 255, 0.04); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12); }

.status-checks {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 28px;
}

.status-check {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.03);
}

.status-dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--muted);
}

.status--ok .status-dot { background: var(--lp-up, #5bd6a0); box-shadow: 0 0 8px rgba(91, 214, 160, 0.6); }
.status--warn .status-dot { background: #e0c477; box-shadow: 0 0 8px rgba(224, 196, 119, 0.6); }
.status--down .status-dot { background: var(--lp-down, #e0637f); box-shadow: 0 0 8px rgba(224, 99, 127, 0.6); }

.status-check-label {
  flex: 0 0 auto;
  color: var(--text);
  font-size: 0.86rem;
  font-weight: 600;
}

.status-check-detail {
  color: var(--muted);
  font-family: var(--numeric);
  font-size: 0.78rem;
  margin-left: auto;
  text-align: right;
}

/* The status page's feed inspector: every wire entry's key, so a wrong entry
   can be ruled on from the dashboard. Reference data, collapsed by default. */
.feed-inspector {
  margin-top: 26px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.02);
}

.feed-inspector > summary {
  padding: 10px 16px;
  color: var(--muted);
  cursor: pointer;
  font-size: 0.8rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  user-select: none;
}

.feed-inspector > summary:hover { color: var(--text); }

.feed-inspector-note {
  margin: 4px 16px 10px;
  color: var(--muted);
  font-size: 0.78rem;
}

.feed-inspector-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 16px;
  border-top: 1px solid var(--border);
}

.feed-inspector-key {
  color: var(--text);
  font-family: var(--numeric);
  font-size: 0.72rem;
  overflow-wrap: anywhere;
  user-select: all;
}

.feed-inspector-text {
  color: var(--muted);
  font-size: 0.8rem;
}

.banner-note {
  margin: 18px 0 0;
  color: var(--muted);
  font-size: 0.86em;
}

/* A short fetch is still long enough to deserve a deliberate arrival state. */
#status {
  display: grid;
  justify-items: center;
  gap: 10px;
  width: min(460px, calc(100% - 40px));
  min-height: 148px;
  margin: clamp(26px, 7vh, 76px) auto clamp(48px, 12vh, 128px);
  padding: 28px 24px;
  border: 1px solid color-mix(in srgb, var(--hairline) 88%, transparent);
  border-radius: 12px;
  background: rgb(13 17 26 / 48%);
  box-shadow: inset 0 1px rgb(255 255 255 / 4%);
  text-align: center;
  color: var(--muted);
  font-family: var(--numeric);
}

.status-eyebrow {
  color: var(--stat-accent);
  font-size: 0.64rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

.status-message {
  color: var(--text);
  font-size: 0.78rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

#status:not(.error) .status-message::after {
  content: "";
  display: inline-block;
  width: 1.6em;
  text-align: left;
  vertical-align: bottom;
  animation: status-dots 1.2s infinite;
}

@keyframes status-dots {
  0% { content: ""; }
  25% { content: "."; }
  50% { content: ".."; }
  75%, 100% { content: "..."; }
}

/* An error retains the same footing but speaks as a readable sentence. */
#status.error {
  display: block;
  min-height: 0;
  border-color: color-mix(in srgb, var(--lp-down) 48%, var(--hairline));
  color: var(--text);
  font-family: var(--font-display);
  font-size: 0.94rem;
  letter-spacing: 0.01em;
  line-height: 1.5;
  text-transform: none;
}

#status.error::before {
  display: block;
  margin-bottom: 8px;
  color: var(--lp-down);
  content: "Ladder unavailable";
  font-family: var(--numeric);
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}


body[data-ladder-phase="live"] .clock-value { color: var(--accent); }
body[data-ladder-phase="ended"] .clock-value { color: var(--muted); }

/* The clock heats up as the event burns down. */
body[data-ladder-phase="live"][data-clock-urgency="calm"] .clock-value {
  color: #6fce8f;
}

body[data-ladder-phase="live"][data-clock-urgency="soon"] .clock-value {
  color: #e0c477;
}

body[data-ladder-phase="live"][data-clock-urgency="final"] .clock-value {
  color: #e06c5e;
}

/*
 * Final day: the same urgency language as the countdown, extended onto the
 * board. Consequence, not decoration — denser separators, a warmer board rim,
 * a slightly harder leader shine. Figures stay on the numeric font at full
 * contrast; nothing here dims a number.
 */
body[data-ladder-phase="live"][data-clock-urgency="final"] #board {
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, #e06c5e 28%, transparent),
    inset 0 -2px 0 color-mix(in srgb, #e06c5e 45%, transparent);
  background:
    linear-gradient(
      180deg,
      color-mix(in srgb, #e06c5e 4.5%, transparent),
      transparent 18%
    );
}

body[data-ladder-phase="live"][data-clock-urgency="final"] .ladder-row {
  border-bottom-color: color-mix(in srgb, #e06c5e 18%, var(--hairline));
}

body[data-ladder-phase="live"][data-clock-urgency="final"] .ladder-row.is-leader::after {
  background: linear-gradient(
    105deg,
    transparent 38%,
    rgba(224, 108, 94, 0.1) 46%,
    rgba(240, 194, 80, 0.22) 50%,
    rgba(224, 108, 94, 0.1) 54%,
    transparent 62%
  );
  background-size: 220% 100%;
  background-repeat: no-repeat;
  animation: leader-shine 4.2s ease-in-out infinite;
}

body[data-ladder-phase="live"][data-clock-urgency="final"] .masthead-clock {
  animation: final-clock-breath 2.8s ease-in-out infinite;
}

@keyframes final-clock-breath {
  0%, 100% { filter: brightness(1); }
  50% { filter: brightness(1.12); }
}

@media (max-width: 560px) {
  .masthead-clock { gap: 12px; }
  .clock-sep { display: none; }
}




/*
 * The ladder page breaks out of the tracker's 1200px measure: a standings board
 * is a wide surface, so it takes nearly the full viewport with generous gutters,
 * while the tracker page keeps its reading-friendly column.
 */
/*
 * The whole ladder reads 20% larger. `zoom` rather than a font-size bump
 * because this sheet is mostly px (332 of them against 34 rem), so scaling
 * the root would move the text and leave every rule, gap, crest and bar
 * behind. Zoom scales the layout itself, so the board stays in proportion.
 *
 * An auto-width block inside a zoomed box resolves against its containing
 * block divided by the zoom, so `main` still lands exactly on the viewport
 * and nothing overflows sideways; only the max-width cap moves, and it only
 * applies on a display wide enough not to care.
 *
 * The countdown is deliberately exempt -- it was already sized as the loudest
 * thing on the page -- so it divides the zoom straight back out. Nested zoom
 * multiplies, so 1.2 x 1/1.2 leaves the clock exactly where it was.
 */
body:has(#ladderShell) main,
body:has(.ladder-masthead) main {
  zoom: var(--ladder-zoom);
}

.masthead-clock {
  zoom: calc(1 / var(--ladder-zoom));
}

body:has(#ladderShell) main {
  width: calc(var(--page-width) / var(--ladder-zoom));
  max-width: calc(var(--page-width) / var(--ladder-zoom));
  margin: 0 auto;
  /* The LP pills hang left of the board (see .lp-pill), so the measure
     reserves their lane: the pill (~168px rendered) plus its 8px stand-off
     plus air, so the pill is fully on screen at every width that shows it.
     Divided back out of the zoom like the width above, so the lane renders
     184px whatever the zoom does. The header and the footer stay on the
     measure's own edges; only the board sits one lane in from the left. */
  padding-left: calc(184px / var(--ladder-zoom));
  padding-right: 0;
}

#ladderShell {
  width: 100%;

  animation: shell-in 640ms var(--ease);
}

@keyframes shell-in {
  from {
    opacity: 0;
    transform: translateY(14px);
  }
}

.ladder-topbar {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 24px;
  flex-wrap: wrap;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--border);
}

.prize {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--numeric, monospace);
  font-size: 0.88rem;
  color: var(--muted, #818a99);
  letter-spacing: 0.02em;
}

.refresh-indicator b {
  color: var(--accent, #d8ba6b);
  font-weight: 600;
}

.ladder-topbar-right {
  display: flex;
  align-items: center;
  gap: 16px;
}

#showWinners {
  background: var(--panel);
  border: 1px solid var(--accent);
  color: var(--accent);
  padding: 7px 12px;
  border-radius: 6px;
  font-size: 0.9em;
  cursor: pointer;
}

#showWinners:hover {
  background: rgba(10, 200, 185, 0.1);
}

.countdown {
  color: var(--text);
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: 0.9em;
  white-space: nowrap;
}


.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}




.ladder-head,
.ladder-row {
  display: grid;


  grid-template-columns:
    minmax(280px, 1fr)
    /* Exactly five 34px icons + four 5px gaps: the column never stretches
       past its content, so no dead air between champions and stats. Five
       rather than six since the board grew 20% and gained a K/D column --
       the sixth-most-played champion is the cheapest thing on the row. */
    190px
    88px
    72px
    76px
    66px
    78px;
  align-items: center;
  /* Six stat columns at this scale leave the player column nothing to spare:
     the tracks and the gap are trimmed so a full Riot ID still fits without
     ellipsis. */
  gap: 13px;
  padding: 0 22px;
}

.ladder-head {
  color: color-mix(in srgb, var(--muted) 60%, var(--text));
  font-size: 0.72em;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}

/*
 * Registration board: a roster, not a scoreboard (js/ladder/board.js). The
 * row renders no live cells, so it lays out on the player track alone instead
 * of the live seven, and the head hides the columns that have nothing to say
 * before the race starts.
 */
.ladder-row--roster {
  grid-template-columns: minmax(280px, 1fr);
}

.ladder-head [hidden] {
  display: none;
}

/* The signup-rank qualifier takes the track the LIVE chip and movement
   arrows own in a scored row, so the row's geometry never changes. */
.signup-rank-note {
  color: var(--muted);
  font-size: 0.72em;
  letter-spacing: 0.05em;
  white-space: nowrap;
}

.ladder-row {
  position: relative;
  min-height: 64px;
  /* The LP pill hangs in the margin, so nothing may clip it. */
  overflow: visible;
  /* Softer than the token: ordinary separators should structure, not box in.
     Streak rows still separate themselves via their accent rails. */
  border-bottom: 1px solid color-mix(in srgb, var(--hairline) 55%, transparent);
  /* Tier as environment: every row is washed in its own tier's colour,
     grading cold-to-warm down the ladder without a single badge. */
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--tier-color, var(--muted)) 5%, transparent),
    transparent 42%
  );
  box-shadow: inset 2px 0 0
    color-mix(in srgb, var(--tier-color, var(--muted)) 32%, transparent);
  transition:
    transform 240ms var(--ease),
    box-shadow 240ms var(--ease),
    background 200ms ease;
}

/*
 * Hover popout: the row lifts out of the table like a broadcast lower-third —
 * slides forward, takes a tier-tinted frame and a drop shadow, and its ghost
 * numeral lights up in the tier colour. Layout never changes; only transform,
 * shadow and paint move.
 */
@media (hover: hover) {
  .ladder-row:hover {
    transform: translateX(6px);
    background:
      linear-gradient(
        90deg,
        color-mix(in srgb, var(--tier-color, var(--muted)) 12%, transparent),
        transparent 55%
      ),
      var(--panel);
    box-shadow:
      inset 2px 0 0 var(--tier-color, var(--muted)),
      0 0 0 1px color-mix(in srgb, var(--tier-color, var(--muted)) 28%, transparent),
      0 14px 34px rgba(3, 6, 10, 0.5);
    z-index: 3;
  }
}

/*
 * The rank is the oversized numeral sitting in the row's backdrop — promoted
 * from decoration to the primary rank display when the number column left.
 * The podium tints it; hovering ignites it in the tier colour.
 */
.ladder-row::before {
  content: attr(data-rank);
  position: absolute;
  left: 12px;
  top: 50%;
  translate: 0 -50%;
  width: 78px;
  text-align: center;
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: 54px;
  font-weight: 200;
  line-height: 1;
  letter-spacing: -0.05em;
  color: color-mix(in srgb, var(--text) 30%, transparent);
  pointer-events: none;
  transition: color 260ms ease;
}

.ladder-row.is-podium::before { color: color-mix(in srgb, var(--tier-silver) 80%, var(--text)); }
.ladder-row.is-podium:first-child::before { color: var(--tier-gold); }
.ladder-row.is-podium:nth-child(3)::before { color: var(--tier-bronze); }

@media (hover: hover) {
  .ladder-row:hover::before {
    color: color-mix(in srgb, var(--tier-color, var(--muted)) 55%, transparent);
  }
}


/* The leader keeps the slow gold shine; the podium speaks through the numeral. */


.ladder-row.is-leader::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    105deg,
    transparent 40%,
    rgba(240, 194, 80, 0.07) 47%,
    rgba(240, 194, 80, 0.16) 50%,
    rgba(240, 194, 80, 0.07) 53%,
    transparent 60%
  );
  background-size: 220% 100%;
  background-repeat: no-repeat;
  
  animation: leader-shine 6.5s ease-in-out infinite;
}

@keyframes leader-shine {
  0%, 22% { background-position: 100% 0; }
  68%, 100% { background-position: 0% 0; }
}




/*
 * The LP pill lives just outside the table edge — close enough that it
 * clearly belongs to its row, far enough to stay secondary to the row's
 * own content. The lane it hangs in is reserved by main's padding-left, so
 * the pill is fully on screen at every width that renders it (below 900px
 * the pill is hidden and the lane is released).
 *
 * One pill states one figure, fully labelled — the race score or, when the
 * LP view toggle above the board asks for it, the season climb — so the
 * pill's width stays a single score line in either view.
 */
.lp-pill {
  position: absolute;
  right: calc(100% + 8px);
  top: 50%;
  translate: 0 -50%;
  display: block;
  text-align: right;
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: 0.92em;
  font-weight: 600;
  padding: 4px 9px;
  border-radius: 7px;
  border: 1px solid;
  white-space: nowrap;
}

.lp-arrow {
  font-style: normal;
  font-size: 0.72em;
  line-height: 1;
  /* The inline flow has no flex gap; keep the arrow's stand-off. */
  margin-right: 4px;
}

.lp-pill.lp-up {
  color: var(--lp-up);
  border-color: rgba(91, 214, 160, 0.38);
  background: rgba(91, 214, 160, 0.1);
}

.lp-pill.lp-down {
  color: var(--lp-down);
  border-color: rgba(224, 99, 127, 0.38);
  background: rgba(224, 99, 127, 0.1);
}

.lp-pill.lp-flat {
  color: var(--muted);
  border-color: var(--border);
  background: transparent;
}

/*
 * The LP view toggle: the switch for which figure the margin pills show.
 * It sits above the board, inside the panel it governs, and borrows the
 * Ladder/Games tabs' vocabulary — same face, gold underline on the active
 * side — so a view switch reads as one whatever it switches. Right-aligned:
 * the tabs own this row's left, and the pills hang off the board's left
 * edge below, so the switch sits clear of the figure it changes. Compact
 * paddings keep both labels on one row at 400px.
 */
.lp-view-toggle {
  display: flex;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: 2px;
  margin: 6px 0 10px;
}

.lp-view-toggle .view-tab {
  padding: 7px 12px;
  font-size: 0.72rem;
}

/* A roster has no LP to present, so the figure switch waits with the race.
   The phase lives on body[data-ladder-phase] (js/ladder/page.js). */
body[data-ladder-phase="registration"] .lp-view-toggle { display: none; }

/* It is member chrome too: a viewer the session has not answered for as an
   authorized member sees no figure to switch. Ships hidden by this rule and
   revealed by the marker (js/site/session-nav.js), so the gated page a
   stranger sees carries nothing of the board's controls. */
body:not([data-member-session="true"]) .lp-view-toggle { display: none; }

.col-player {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
  /* The rank numeral owns the lane to the left of this cell. */
  padding-left: 86px;
}

/*
 * Four columns: the name takes whatever is left, the optional LIVE chip and
 * movement arrow sit in an auto track, and badge + LP get fixed tracks pinned
 * to the cell's right edge. Fixed widths mean every row's rank figures stack
 * on the same gridlines, however long the name above them runs.
 */
.player-line {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto 38px 66px;
  align-items: center;
  gap: 8px;
}

.player-meta {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

.player-name {
  color: var(--text);
  min-width: 0;
  flex: 0 1 auto;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 500;
  font-size: 1.06em;
  letter-spacing: 0.01em;
}

.player-name:hover {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.player-name:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 2px;
}


.rank-crest {
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  display: block;
}

.rank-crest.is-unranked,
.rank-crest.is-placements {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 1px dashed var(--border);
}


.tier-iron { --tier-color: var(--tier-iron); }
.tier-bronze { --tier-color: var(--tier-bronze); }
.tier-silver { --tier-color: var(--tier-silver); }
.tier-gold { --tier-color: var(--tier-gold); }
.tier-platinum { --tier-color: var(--tier-platinum); }
.tier-emerald { --tier-color: var(--tier-emerald); }
.tier-diamond { --tier-color: var(--tier-diamond); }
.tier-master { --tier-color: var(--tier-master); }
.tier-grandmaster { --tier-color: var(--tier-grandmaster); }
.tier-challenger { --tier-color: var(--tier-challenger); }
.tier-unranked { --tier-color: var(--muted); }


.rank-badge {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px;
  padding: 2px 5px;
  border-radius: 4px;
  border: 1px solid color-mix(in srgb, var(--tier-color, var(--muted)) 45%, transparent);
  background: color-mix(in srgb, var(--tier-color, var(--muted)) 16%, transparent);
  color: var(--tier-color, var(--muted));
  font-family: var(--numeric);
  font-size: 0.72em;
  font-weight: 600;
  letter-spacing: 0.02em;
  line-height: 1.3;
}

/* The unranked wordmark owns both rank tracks — it cannot fit in the badge's
 * 38px alone, and there is no LP to sit beside it. Placements text has the
 * same problem ("Placements 3/5"), so it takes the same span. */
.rank-badge.is-unranked,
.rank-badge.is-placements {
  grid-column: 3 / 5;
  justify-self: stretch;
  letter-spacing: 0.14em;
  font-size: 0.64em;
}

.rank-badge.is-placements {
  /* Slightly tighter tracking: digits need less air than UNRANKED. */
  letter-spacing: 0.06em;
}


.rank-lp {
  flex: 0 0 auto;
  color: var(--text);
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: 0.92em;
  font-weight: 400;
  text-align: right;
  white-space: nowrap;
}

.position-delta {
  font-family: var(--numeric);
  font-size: 0.72em;
  font-variant-numeric: tabular-nums;
}

.delta-up { color: var(--lp-up); }
.delta-down { color: var(--lp-down); }

.col-champions {
  display: flex;
  gap: 5px;
  flex-wrap: nowrap;
}

.champion-icon {
  width: 34px;
  height: 34px;
  
  flex: 0 0 auto;
  border-radius: 5px;
  border: 1px solid var(--hairline);
  background: var(--panel);
  display: block;
  transition: transform 160ms ease, border-color 160ms ease;
}


.champion-icon:first-child {
  border-color: var(--accent);
}

.champion-icon:hover {
  transform: translateY(-3px) scale(1.12);
  border-color: var(--accent);
  z-index: 2;
}

.champion-empty {
  color: var(--muted);
  font-size: 0.76em;
}


.lp-up { color: var(--lp-up); }
.lp-down { color: var(--lp-down); }

.lp-flat { color: var(--muted); }

.col-games, .col-winrate, .col-deaths, .col-kd, .col-cs {
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: 1.08em;
  color: var(--text);
  text-align: center;
  position: relative;
  padding-top: 11px;
  padding-bottom: 6px;
}


/* Deaths stay negative, but desaturated — the pink/red family is already
   carrying losses, LP drops and LIVE, and this column should not out-shout
   those state signals. */
.col-deaths { color: color-mix(in srgb, var(--lp-down) 62%, var(--muted)); }
.col-winrate { color: var(--stat-accent); }
/* K/D reads as a positive rate, so it takes the stat accent rather than the
   deaths column's desaturated red -- a high ratio is a good thing. */
.col-kd { color: var(--stat-accent); }


.ladder-head .col-games,
.ladder-head .col-winrate,
.ladder-head .col-deaths,
.ladder-head .col-kd,
.ladder-head .col-cs {
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  color: var(--muted);
  text-align: center;
  padding-bottom: 0;
}

.ladder-head .col-games::after,
.ladder-head .col-winrate::after,
.ladder-head .col-deaths::after,
.ladder-head .col-kd::after,
.ladder-head .col-cs::after {
  content: none;
}




.col-games::after,
.col-winrate::after,
.col-deaths::after,
.col-kd::after,
.col-cs::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 1px;
  width: 100%;
  height: 6px;
  
  background: linear-gradient(to top, currentColor, transparent);
  filter: blur(2.5px);
  opacity: 0.22;
  transform: scaleX(var(--fill, 0));
  transform-origin: center;
  transition: transform 700ms cubic-bezier(.2, .8, .2, 1);
}


.is-best {
  font-weight: 700;
}

.is-best::before {
  content: "";
  position: absolute;
  top: 0;
  
  left: 50%;
  transform: translateX(-50%);
  width: 14px;
  height: 10px;
  background: var(--crown-gold);
  opacity: 0.95;
  -webkit-mask-image: var(--crown);
  mask-image: var(--crown);
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
}


/* The column leader is marked by the crown above the figure alone. The gold
   bar and its glow used to sit under the stat as well, which read as a second
   highlight competing with the crown; the crown says it once. */


.is-entering .col-games::after,
.is-entering .col-winrate::after,
.is-entering .col-deaths::after,
.is-entering .col-kd::after,
.is-entering .col-cs::after {
  animation: bar-grow 760ms cubic-bezier(.2, .8, .2, 1) backwards;
  animation-delay: calc(var(--row-index, 0) * 45ms + 260ms);
}

@keyframes bar-grow {
  from { transform: scaleX(0); }
  to { transform: scaleX(var(--fill, 0)); }
}


.is-entering .is-best::before {
  animation: crown-drop 460ms cubic-bezier(.2, .8, .2, 1) backwards;
  animation-delay: calc(var(--row-index, 0) * 45ms + 420ms);
}

@keyframes crown-drop {
  from { opacity: 0; transform: translateX(-50%) translateY(-5px); }
  to { opacity: 0.95; transform: translateX(-50%); }
}


/*
 * The stat cells' history buttons. The button lives inside the role="cell"
 * span (see statCell in board.js) and fills it, so the click target is the
 * whole cell, the --fill bar and the is-best crown stay on the span, and the
 * grid never gets its geometry from a focusable element. The button resets
 * to the number itself; the only affordance the resting state shows is the
 * pointer cursor, because five permanently underlined numbers would turn the
 * board into a wall of chrome. On hover or focus the figure underlines
 * dotted — an invitation, not a link.
 */
.stat-toggle {
  appearance: none;
  background: none;
  border: 0;
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font: inherit;
  color: inherit;
  letter-spacing: inherit;
  font-variant-numeric: inherit;
  cursor: pointer;
  text-decoration: none;
}

.stat-toggle:hover,
.stat-toggle:focus-visible {
  text-decoration: underline dotted 1px;
  text-underline-offset: 4px;
}

.stat-toggle:focus-visible {
  outline: 2px solid var(--stat-accent);
  outline-offset: 1px;
}

/* A row hosting an open popover lifts above its neighbours, so the panel
   paints over the rows below instead of sliding under them. The transform
   also cancels: at sheet widths the panel is position:fixed, and any
   transformed ancestor — including this row's hover lift — would become its
   containing block and drag the sheet off the viewport bottom. */
.ladder-row.has-stat-history {
  transform: none;
  z-index: 30;
}

/*
 * The per-game history popover: the panel is the chart — a trend polyline,
 * the cell's own average as a dashed reference line, best and worst games as
 * gold and red dots — over a strip of champion portraits, one per game,
 * aligned beneath their dots. Nothing else. The only visible text is the
 * hover readout, which exists while a game is active and not otherwise. It
 * is appended inside the cell it belongs to and anchored per column — the
 * CS cell sits on the board's right edge, so its panel opens leftward into
 * the board rather than off the page.
 */
.stat-history {
  position: absolute;
  top: calc(100% + 8px);
  z-index: 30;
  width: 264px;
  padding: 10px 12px;
  border: 1px solid color-mix(in srgb, var(--stat-accent) 30%, var(--hairline));
  border-radius: 10px;
  background: color-mix(in srgb, var(--panel, #10141c) 94%, transparent);
  box-shadow: 0 18px 40px rgba(3, 6, 10, 0.55);
  backdrop-filter: blur(10px);
  text-align: left;
}

.stat-history--cs { right: 0; }
.stat-history--deaths,
.stat-history--kd { left: 50%; transform: translateX(-50%); }
.stat-history--winrate { left: 0; }

.stat-history.is-flipped {
  top: auto;
  bottom: calc(100% + 8px);
}

/* The one control on the panel, parked in its corner: a glyph, not prose. */
.stat-history-close {
  position: absolute;
  top: 5px;
  right: 5px;
  z-index: 1;
  appearance: none;
  background: none;
  border: 0;
  padding: 3px 5px;
  font-size: 0.8em;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
  border-radius: 4px;
}

.stat-history-close:hover { color: var(--text); }

.stat-history-close:focus-visible {
  outline: 2px solid var(--stat-accent);
  outline-offset: 1px;
}

.stat-history-chart {
  display: block;
  width: 100%;
  height: auto;
  /* The outermost strip tiles centre on points that sit padX=6 inside the
     viewBox, so they hang ~2.5px past either edge; letting them paint into
     the panel's 12px padding keeps every portrait whole under its dot. */
  overflow: visible;
}

/* The lines are display graphics: a stroke passing through a dot must not
   steal the pointer from the band beneath it. */
.stat-history-trend,
.stat-history-ref,
.stat-history-sep {
  pointer-events: none;
}

.stat-history-trend {
  fill: none;
  stroke: var(--stat-accent);
  stroke-width: 1.6;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.stat-history-ref {
  stroke: color-mix(in srgb, var(--text) 45%, transparent);
  stroke-width: 1;
  stroke-dasharray: 4 3;
}

/* The hairline that separates the plot band from the portrait strip — faint
   enough to read as one panel, present enough that the two bands read as
   related bands rather than one crowded chart. */
.stat-history-sep {
  stroke: color-mix(in srgb, var(--text) 18%, transparent);
  stroke-width: 1;
}

/* The bands are the mouse targets — invisible, spanning the full column
   (plot and strip alike), tiled edge to edge so a dense series stays
   hittable; everything above them is display only. */
.stat-history-band {
  fill: transparent;
}

/* Dots and portraits alike are display graphics: the bands own every hover,
   so a mark can never shadow its neighbour's target — and pointing straight
   at a portrait still lights up its dot through the band beneath it. */
.stat-history-marks,
.stat-history-strip {
  pointer-events: none;
}

.stat-history-dot {
  fill: color-mix(in srgb, var(--text) 55%, transparent);
}

.stat-history-point.is-best .stat-history-dot { fill: var(--crown-gold); }
.stat-history-point.is-worst .stat-history-dot { fill: var(--lp-down); }

.stat-history-point.is-active .stat-history-dot {
  r: 4.2;
  fill: var(--stat-accent);
  stroke: var(--text);
  stroke-width: 1.2;
}

/* The strip: each game's champion in a hairline tile like the board's
   portraits, parked under its own dot. The active tile lifts — the same
   grow gesture the dot makes, so the paired bands read as one column;
   dimming every resting portrait would tax the whole strip to pay for one
   hover. */
.stat-history-slot {
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 120ms var(--ease);
}

.stat-history-slot.is-active {
  transform: scale(1.2);
}

.stat-history-frame {
  fill: var(--panel, #10141c);
  stroke: var(--hairline);
}

/* The readout is a display, not a surface: the pointer passes through it to
   the band beneath, or a pill that flips under the cursor would block the
   very point it is describing. */
.stat-history-readout {
  pointer-events: none;
}

.stat-history-readout rect {
  fill: var(--panel, #10141c);
  stroke: color-mix(in srgb, var(--stat-accent) 45%, transparent);
}

.stat-history-readout text {
  fill: var(--text);
  font-family: var(--numeric);
  font-size: 7.5px;
  letter-spacing: 0.02em;
}


.live-chip {
  flex: 0 0 auto;
  cursor: pointer;
  font-family: inherit;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.62em;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: #ff6b81;
  background: rgba(224, 99, 127, 0.14);
  border: 1px solid rgba(224, 99, 127, 0.4);
  padding: 2px 7px;
  border-radius: 999px;
  white-space: nowrap;
  transition:
    transform 200ms cubic-bezier(.16, 1, .3, 1),
    background 200ms ease,
    border-color 200ms ease;
}

.live-chip:hover {
  transform: translateY(-1px);
  border-color: rgba(224, 99, 127, 0.65);
}

.live-chip:active {
  transform: translateY(0) scale(0.95);
}

.live-chip:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.live-chip i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #ff4d67;
  animation: live-pulse 1.6s ease-in-out infinite;
}

@keyframes live-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.35; transform: scale(0.78); }
}


/* The record reads as a scoreline -- 20-16 -- so the two numbers carry the
   meaning and the separator stays out of the way. Brighter than the shared
   LP tokens on purpose: those mark a delta somewhere in the row, this is the
   record itself and should read first. */
.col-games .wins { color: #3ce89a; font-weight: 700; }
.col-games .losses { color: #ff5b7c; font-weight: 700; }

.col-games .wl-split {
  color: var(--muted);
  font-size: 0.9em;
  margin: 0 1px;
}

.ladder-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}




/*
 * The way back: a Hextech inventory slot. Hexagonal plate, gold hairline,
 * dark glass fill — the arrow reads as a socketed rune, and hovering floods
 * the slot with gold like a picked-up item.
 */
.back-float {
  position: fixed;
  /* Below the shared header, never over it: the header is ~68px tall on
     desktop, and at laptop widths this button used to sit on the brand
     wordmark. The two-row narrow header is taller and re-offsets below. */
  top: 82px;
  left: clamp(16px, 3vw, 32px);
  z-index: 50;
  display: grid;
  place-items: center;
  width: 56px;
  height: 62px;
  clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%);
  text-decoration: none;
  color: #d8ba6b;
  background:
    linear-gradient(160deg, rgba(216, 186, 107, 0.14), rgba(255, 255, 255, 0.03) 46%),
    rgba(10, 13, 20, 0.72);
  box-shadow:
    inset 0 0 0 1px rgba(216, 186, 107, 0.38),
    inset 0 1px 1px rgba(255, 255, 255, 0.08),
    0 14px 34px rgba(3, 6, 10, 0.5);
  backdrop-filter: blur(12px);
  font-size: 1.15rem;
  transition:
    scale 480ms cubic-bezier(.16, 1, .3, 1),
    box-shadow 480ms cubic-bezier(.16, 1, .3, 1),
    background 480ms cubic-bezier(.16, 1, .3, 1),
    color 480ms cubic-bezier(.16, 1, .3, 1);
}

.back-float__label {
  position: absolute;
  left: calc(100% + 10px);
  white-space: nowrap;
  color: #cfd5df;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  opacity: 0;
  transform: translateX(-6px);
  transition:
    opacity 420ms cubic-bezier(.16, 1, .3, 1),
    transform 480ms cubic-bezier(.16, 1, .3, 1);
}

.back-float:hover .back-float__label,
.back-float:focus-visible .back-float__label {
  opacity: 1;
  transform: none;
}

.back-float:hover {
  scale: 1.06;
  color: #171207;
  background:
    linear-gradient(160deg, rgba(240, 214, 140, 0.9), rgba(216, 186, 107, 0.75) 55%),
    rgba(216, 186, 107, 0.9);
  box-shadow:
    inset 0 0 0 1px rgba(240, 214, 140, 0.9),
    0 18px 40px rgba(3, 6, 10, 0.55),
    0 0 24px rgba(216, 186, 107, 0.35);
}

.back-float:active {
  scale: 0.92;
}

.back-float:focus-visible {
  outline: 2px solid #e0c477;
  outline-offset: 3px;
}

@media (max-width: 640px) {
  /* The shared header reflows to two rows here (brand+actions, then nav) and
     measures ~91px, so the hexagon's desktop offset would land on the nav. */
  .back-float { top: 104px; }
}




@media (max-width: 1080px) {
  .champion-icon:nth-child(n + 6) { display: none; }
}

@media (max-width: 1000px) {
  .champion-icon:nth-child(n + 5) { display: none; }
}

@media (max-width: 940px) {
  .champion-icon:nth-child(n + 4) { display: none; }
}


@media (max-width: 900px) {
  .ladder-head,
  .ladder-row {
    grid-template-columns: minmax(140px, 1fr) 62px 62px;
  }

  /* The floating score bug needs margin the narrow layout does not have. */
  .lp-pill { display: none; }
  /* The toggle only chooses what the pills show; with no pills it would be a
     switch that visibly does nothing, so it leaves with them. */
  .lp-view-toggle { display: none; }

  /* No pills, so the lane their gutter reserved goes back to the board. */
  body:has(#ladderShell) main { padding-left: 0; }

  .ladder-row::before {
    left: 6px;
    width: 56px;
    font-size: 42px;
  }

  .col-player { padding-left: 64px; }

  .col-champions,
  .col-deaths,
  .col-kd,
  .col-cs {
    display: none;
  }

  /* What survives here is the winrate cell (games carries no popover), and a
     panel anchored to a 62px track reads as a misprint at this width. It
     becomes a bottom sheet instead: full measure, thumb-reachable, closed by
     the same Escape, ✕ and outside-tap. Fixed rather than absolute because
     the narrow board is the one view where the anchoring cell can be scrolled
     half off-screen; the row's z-index lift keeps it above later rows. The
     width needs !important because the series-sized panel sets its own
     inline width, and a 686px sheet would not be a sheet. */
  .stat-history,
  .stat-history.is-flipped {
    position: fixed;
    top: auto;
    right: 12px;
    bottom: 12px;
    left: 12px;
    width: auto !important;
    transform: none;
  }
}

@media (max-width: 560px) {
  .ladder-head,
  .ladder-row {
    grid-template-columns: minmax(96px, 1fr);
    gap: 8px;
    padding: 0 4px;
  }

  .ladder-row::before {
    left: 2px;
    width: 44px;
    font-size: 34px;
  }

  .col-player { padding-left: 48px; }

  .col-games,
  .col-winrate {
    display: none;
  }

  /* The LP figure is the first sacrifice on a phone; badge stays. */
  .player-line {
    grid-template-columns: minmax(0, 1fr) auto 38px;
  }

  .rank-lp {
    display: none;
  }

  .ladder-topbar {
    align-items: flex-start;
    flex-direction: column;
    gap: 12px;
  }
}



@media (prefers-reduced-motion: reduce) {
  .ladder-row,
  .ladder-row.is-entering,
  .ladder-row.is-leader::after,
  .live-chip i,
  .is-best,
  .is-entering .is-best::before,
  .is-entering .champion-icon,
  .champion-icon,
  #ladderShell,
  .clock-value.is-ticking,
  .back-float,
  .masthead-clock {
    animation: none !important;
    transition: none !important;
    transform: none !important;
    opacity: 1 !important;
  }

  body[data-ladder-phase="live"][data-clock-urgency="final"] .masthead-clock {
    filter: none;
  }

  #status:not(.error) .status-message::after {
    animation: none;
    content: "...";
  }
}




/*
 * The expanded live game. A card, not a table continuation: the panel belongs
 * to one player's row, so it floats inside the board with its own chrome and
 * carries a faint blue-over-red wash matching blue-side / red-side order.
 */
.live-inline {
  margin: 2px 12px 16px;
  border: 1px solid var(--hairline);
  border-radius: 14px;
  background:
    linear-gradient(180deg, rgba(107, 143, 224, 0.05), transparent 30%),
    linear-gradient(0deg, rgba(224, 99, 127, 0.05), transparent 30%),
    rgba(255, 255, 255, 0.015);
  box-shadow: 0 14px 34px rgba(3, 6, 10, 0.35);
  overflow: hidden;
  animation: live-open 320ms cubic-bezier(.2, .8, .2, 1);
  transform-origin: top center;
}

@keyframes live-open {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: none; }
}

.live-inline-head {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px clamp(14px, 2vw, 20px) 12px;
  border-bottom: 1px solid var(--hairline);
  animation: part-in 380ms var(--ease) backwards;
}

.live-flag {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: #ff6b81;
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.live-flag i {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #ff4d67;
  animation: live-pulse 1.6s ease-in-out infinite;
}

.live-inline-title {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--muted);
  font-size: 0.84em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The clock is the point of a live view, so it reads as a figure, not a chip. */
.live-clock {
  flex: 0 0 auto;
  margin-left: auto;
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: clamp(1.15rem, 2.2vw, 1.5rem);
  font-weight: 250;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--text);
}

.live-clock b {
  font-weight: inherit;
}

.live-teams {
  display: grid;
  gap: 18px;
  padding: 14px clamp(12px, 2vw, 20px) 18px;
}

/* The centre spine only earns its width once both teams sit side by side. */
.live-vs { display: none; }

@media (min-width: 720px) {
  .live-teams {
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    gap: clamp(16px, 2.4vw, 28px);
  }

  .live-vs {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    padding: 6px 0;
  }

  .live-vs::before,
  .live-vs::after {
    content: "";
    flex: 1;
    width: 1px;
    background: var(--hairline);
  }

  .live-vs span {
    /* Tracking adds a trailing gap, so pad the left to re-centre the word. */
    padding-left: 0.32em;
    font-family: var(--numeric);
    font-size: 0.58rem;
    font-weight: 700;
    letter-spacing: 0.32em;
    text-transform: uppercase;
    color: color-mix(in srgb, var(--text) 32%, transparent);
  }
}

.live-team {
  border: 1px solid rgba(255, 255, 255, 0.07);
  border-radius: 9px;
  background: rgba(8, 11, 17, 0.5);
}

.live-team-100 { border-color: rgba(72, 142, 210, 0.22); }
.live-team-200 { border-color: rgba(210, 75, 103, 0.2); }

/* The head is the same banner the profile scoreboard uses: a side-tinted
   gradient strip behind a heavy uppercase team name. */
.live-team-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  min-height: 42px;
  padding: 7px 12px 7px 14px;
  margin-bottom: 2px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
}

.live-team-100 .live-team-head {
  background: linear-gradient(90deg, rgba(35, 95, 157, 0.42), rgba(23, 58, 96, 0.24));
}

.live-team-200 .live-team-head {
  background: linear-gradient(90deg, rgba(138, 39, 61, 0.42), rgba(83, 28, 42, 0.25));
}

.live-team-name {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  color: #f0f4f9;
  text-transform: uppercase;
  font-weight: 900;
  font-size: 0.8em;
  letter-spacing: 0.095em;
}

/* The diamond keeps the team colour so the side reads at a glance. */
.live-team-name::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 1.5px;
  rotate: 45deg;
  background: currentColor;
}

.live-team-100 .live-team-name::before { background: #6b8fe0; }
.live-team-200 .live-team-name::before { background: #e0637f; }

.live-bans {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}

.live-bans-label {
  color: #a7b2c0;
  font-size: 0.68em;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin-right: 2px;
}

.live-ban {
  position: relative;
  border-radius: 3px;
  overflow: hidden;
}

.live-team-100 .live-ban { border: 1px solid rgba(107, 184, 244, 0.35); }
.live-team-200 .live-ban { border: 1px solid rgba(236, 113, 139, 0.35); }

/* The strike, not just grey: these champions are out of this game. */
.live-ban::after {
  content: "";
  position: absolute;
  inset: -2px;
  background: linear-gradient(
    to top right,
    transparent 47%,
    rgba(224, 99, 127, 0.9) 49% 51%,
    transparent 53%
  );
}

.live-participants {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
}

.live-participant {
  position: relative;
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 10px;
  padding: 5px 8px;
  border-radius: 8px;
  transition: background 160ms ease;
  animation: part-in 420ms var(--ease) backwards;
  animation-delay: calc(var(--p, 0) * 36ms + 120ms);
}

/* The lobby's assigned position, in its own track so the ten rows read the
   role at the same x even when a client left one blank. The track exists
   only for a team whose payload actually carries positions — Riot's
   spectator endpoint verified 2026-09-18 sends none — so a real lobby never
   reserves a dead gap where an icon cannot be. */
.live-participants.has-roles .live-participant {
  grid-template-columns: 32px 16px minmax(0, 1fr) auto;
}

.live-role {
  width: 16px;
  height: 16px;
  display: block;
}

/* Season games and win rate, right-aligned against the rank figures. The
   slot renders for every row — empty for unranked and placements — so the
   rank cell never changes x between rows. */
.live-games {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  /* Fixed, not min-width: an empty slot (unranked, placements) must occupy
     exactly the track a populated one would, or every row's columns drift. */
  width: 64px;
}

.live-games-rate {
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: 0.82em;
  color: var(--text);
  line-height: 1.1;
}

.live-games-count {
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: 0.64em;
  color: var(--muted);
  line-height: 1.1;
  white-space: nowrap;
}

/* The red side lands a beat after the blue, so the reveal sweeps across. */
.live-team-200 .live-participant {
  animation-delay: calc(var(--p, 0) * 36ms + 200ms);
}

@keyframes part-in {
  from { opacity: 0; transform: translateY(7px); }
}

.live-participant:hover {
  background: rgba(255, 255, 255, 0.03);
}

.live-participant.is-tracked {
  background: rgba(10, 200, 185, 0.09);
  outline: 1px solid rgba(10, 200, 185, 0.35);
}

.live-participant.is-tracked::before {
  content: "";
  position: absolute;
  left: -1px;
  top: 50%;
  transform: translateY(-50%);
  width: 2px;
  height: 60%;
  border-radius: 1px;
  background: var(--accent);
}

.live-champion {
  width: 32px;
  height: 32px;
  border-radius: 6px;
  border: 2px solid
    color-mix(in srgb, var(--tier-color, var(--muted)) 65%, transparent);
  background: var(--panel);
  display: block;
}

.live-champion.tier-unranked {
  border-style: dashed;
  opacity: 0.85;
}

/* Summoner spells and the keystone sit between the name and the season
   figures. Spectator-v5 carries spells and the one chosen keystone — no
   items — so this is the loadout cluster the live panel can honestly show.
   The final track exists only when a participant actually has a loadout:
   without one the row keeps its shorter shape, and a rank cell must never
   wrap to a second line for lack of a track. */
.live-participant.has-spells {
  grid-template-columns: 32px minmax(0, 1fr) auto auto auto;
}

.live-participants.has-roles .live-participant.has-spells {
  grid-template-columns: 32px 16px minmax(0, 1fr) auto auto auto;
}

.live-spells {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.live-spell {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  object-fit: cover;
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: var(--panel);
  display: block;
}

.live-spell.loadout-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  border-style: dashed;
  font-size: 0.52rem;
}

/* The keystone is round art, unlike the square spell frames. */
.live-spell.live-perk {
  border-radius: 50%;
}

.live-names {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.live-summoner {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.86em;
  font-weight: 600;
}

.live-participant.is-tracked .live-summoner {
  color: #9ff0e9;
}

.live-champion-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted);
  font-size: 0.68em;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/*
 * Fixed width so the crest column lands on the same x in every row, and a
 * fixed division column so the roman numerals stack on a shared right edge.
 */
.live-rank-cell {
  justify-self: end;
  /* Fixed width: "Unranked" and "Placements 3/5" are wider than "IV 74 LP",
     and an auto-sized cell here dragged the spells column around per row.
   */
  width: 152px;
  display: grid;
  grid-template-columns: 18px 2.8em max-content;
  justify-content: end;
  align-items: center;
  gap: 6px;
}

.live-crest {
  width: 18px;
  height: 18px;
  display: block;
}

.live-crest.is-empty {
  border-radius: 50%;
  border: 1px dashed var(--border);
}

.live-rank {
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: 0.88em;
  color: var(--muted);
  text-align: right;
  white-space: nowrap;
}

.live-lp {
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: 0.88em;
  color: var(--text);
  white-space: nowrap;
}

.live-chip[aria-expanded="true"] {
  background: rgba(224, 99, 127, 0.26);
}

@media (max-width: 719px) {
  .live-inline {
    margin: 2px 8px 12px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .live-inline,
  .live-flag i,
  .live-inline-head,
  .live-participant {
    animation: none;
  }
}

/*
 * Form guide: the last ten results as win/loss ticks beside the name.
 * Fixed width so every row's ticks occupy the same lane however many games
 * the player has; the strip right-aligns against the badge track.
 *
 * On hover the ticks grow in place into W/L boxes and each game's champion
 * portrait rises above its own box. The portraits are absolutely positioned
 * off the row, so the strip gains width but never height. The width it gains
 * is taken back from the name's ellipsis lane (the strip lives in the
 * player-meta flex row, right of the name) rather than out of the page:
 * the other grid columns keep their fixed tracks, and a name long enough to
 * be near truncation shortens instead of the row reflowing.
 */
.form-strip {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 2px;
  width: 58px;
  position: relative;
  flex: 0 0 auto;
}

.form-game {
  position: relative;
  display: block;
  flex: 0 0 auto;
}

.form-tick {
  display: grid;
  place-items: center;
  width: 4px;
  height: 11px;
  border-radius: 1px;
  font-size: 0;
  font-family: var(--numeric);
  font-weight: 700;
  color: var(--bg);
  transition: width 170ms var(--ease), height 170ms var(--ease), font-size 170ms var(--ease), opacity 170ms var(--ease);
}

.form-game.is-win .form-tick { background: var(--lp-up); opacity: 0.9; }
.form-game.is-loss .form-tick { background: var(--lp-down); opacity: 0.65; }

/* The champion portrait, parked above its own tick. */
.form-champ {
  position: absolute;
  bottom: calc(100% + 3px);
  left: 50%;
  translate: -50% 0;
  width: 22px;
  height: 22px;
  border-radius: 3px;
  border: 1px solid var(--hairline);
  display: block;
  opacity: 0;
  transform: translateY(3px);
  transition: opacity 150ms var(--ease), transform 150ms var(--ease);
  pointer-events: none;
}

@media (hover: hover) {
  .form-strip:hover {
    width: 262px;
    gap: 4px;
  }

  .form-strip:hover .form-tick {
    width: 22px;
    height: 16px;
    font-size: 10px;
    border-radius: 2px;
  }

  .form-strip:hover .form-game.is-loss .form-tick { opacity: 0.95; }
  .form-strip:hover .form-game.is-win .form-tick::after { content: "W"; }
  .form-strip:hover .form-game.is-loss .form-tick::after { content: "L"; }

  .form-strip:hover .form-champ {
    opacity: 1;
    transform: translateY(0);
  }

  .form-strip:hover .form-game.is-loss .form-champ { opacity: 0.7; }
}

/* Hover is the only key that opens the strip. A keyboard or screen reader
   user keeps the W/L reading through the aria-label above. */

@media (max-width: 900px) {
  .form-strip { display: none; }
}

/* The expansion is motion, but the information is not. Under
   prefers-reduced-motion the strip still opens — instantly, with every
   transition stripped — because the champion identities have no second
   channel: suppressing the expansion would suppress the information. A
   single instant reflow is not animation. */
@media (prefers-reduced-motion: reduce) {
  .form-strip,
  .form-strip:hover,
  .form-tick,
  .form-champ {
    transition: none !important;
  }
}

@media (prefers-reduced-motion: reduce) {
  .ladder-row,
  .ladder-row:hover,
  .ladder-row::before,
  .ladder-row:hover::before {
    transform: none !important;
    transition: none !important;
  }
}

/*
 * Streak states. A three-game run stops being a statistic and becomes a
 * story, so the row changes weather: warm and energised for the climbing,
 * cold and quiet for the sliding. Both states paint through .row-fx and
 * share the same structure — a left rail, a fast-fading gradient, a badge —
 * so they read as two settings of one system, not two unrelated effects.
 *
 * Deliberately no streak-length tiers: a 3-run and a 10-run look identical,
 * only the number in the badge changes.
 *
 * Hierarchy is intentional: hot carries a glow and a flickering flame;
 * cold carries neither. Hot is a headline, cold is a footnote.
 */

.ladder-row .row-fx {
  display: none;
}

.ladder-row.is-win-streak .row-fx,
.ladder-row.is-lose-streak .row-fx {
  display: block;
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* --- Hot: amber rail, warm wash that fades fast, lit badge ---------------- */

.ladder-row.is-win-streak {
  box-shadow:
    inset 3px 0 0 #e8762f,
    inset 4px 0 0 rgba(255, 196, 110, 0.5);
}

.ladder-row.is-win-streak .row-fx {
  background: linear-gradient(
    90deg,
    rgba(232, 140, 56, 0.11),
    rgba(232, 140, 56, 0.035) 16%,
    transparent 30%
  );
}

.streak-chip.streak-hot {
  color: #f0a04e;
  border-color: rgba(232, 140, 56, 0.45);
  background: rgba(232, 140, 56, 0.12);
  box-shadow: 0 0 10px rgba(232, 140, 56, 0.22);
}

.streak-flame {
  width: 10px;
  height: 13px;
  display: inline-block;
  background: linear-gradient(180deg, #ffd27a, #e8762f);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 16'%3E%3Cpath d='M6 0C4.6 3.2 1 5.4 1 9.6 1 13 3.2 16 6 16s5-3 5-6.4C11 7 9.4 5.4 8.4 3.6c-.5 1.5-1.2 2.3-2.1 3C6 4.9 6.4 2.4 6 0z' fill='%23fff'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 16'%3E%3Cpath d='M6 0C4.6 3.2 1 5.4 1 9.6 1 13 3.2 16 6 16s5-3 5-6.4C11 7 9.4 5.4 8.4 3.6c-.5 1.5-1.2 2.3-2.1 3C6 4.9 6.4 2.4 6 0z' fill='%23fff'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
  animation: flame-flicker 900ms ease-in-out infinite alternate;
  transform-origin: 50% 90%;
}

@keyframes flame-flicker {
  from { transform: scale(1, 0.94) skewX(-2deg); }
  to { transform: scale(1.06, 1.08) skewX(2deg); }
}

/* The active streak's own tick glows faintly — the form guide points at it. */
.is-win-streak .form-strip .form-game.is-win:last-child .form-tick {
  box-shadow: 0 0 5px rgba(240, 160, 78, 0.55);
}

/* --- Cold: indigo rail, a whisper of a wash, a muted badge ----------------- */
/* Cold momentum: the same structure as hot, one notch quieter everywhere.
   No animation, no desaturation, no row-wide tint — sliding, not broken. */

.ladder-row.is-lose-streak {
  box-shadow:
    inset 3px 0 0 #6a75b8,
    inset 4px 0 0 rgba(139, 147, 200, 0.35);
}

.ladder-row.is-lose-streak .row-fx {
  background: linear-gradient(
    90deg,
    rgba(106, 117, 184, 0.08),
    rgba(106, 117, 184, 0.025) 14%,
    transparent 26%
  );
}

/* --- The chips ------------------------------------------------------------ */

.streak-chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--numeric);
  font-variant-numeric: tabular-nums;
  font-size: 0.68em;
  font-weight: 700;
  padding: 2px 7px;
  border-radius: 999px;
  border: 1px solid;
  white-space: nowrap;
}

/*
 * Cold chip: deliberately unlike the rank badges (which are solid-bordered
 * rectangles). Fainter border, near-transparent fill, and the icon — not the
 * container — carries the colour, so it reads as a momentum note, not a
 * division.
 */
.streak-chip.streak-cold {
  color: color-mix(in srgb, #9aa3cf 85%, transparent);
  border-color: rgba(122, 132, 196, 0.2);
  background: rgba(106, 117, 184, 0.05);
}

/* The cold chip's slide arrow: momentum pointing down and away. The icon
   carries most of the badge's colour, so it stays legible on its fainter
   container. */
.streak-slide {
  width: 10px;
  height: 10px;
  display: inline-block;
  background: linear-gradient(180deg, #cdd3f2, #7d89cf);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 2.5l6.2 6.2M9.5 4.4v5.1H4.4' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 2.5l6.2 6.2M9.5 4.4v5.1H4.4' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
}

@media (prefers-reduced-motion: reduce) {
  .streak-flame {
    animation: none !important;
  }
}




/*
 * Result states. A row whose own game moved it up or down the board wears the
 * result while the reorder plays: a green frame and wash for either outcome,
 * a floating badge with the LP the game was worth, and — for a fall — a red
 * override that gives the loss back its weight. Green is the base state and
 * red is layered on top of it, never a substitute for it, so a defeat reads
 * as "a result happened here" first and "and it went badly" second.
 *
 * Ported from prototypes/ladder-motion/motion.css; the class lifecycle lives
 * in board.js's renderBoard. is-result persists after landing until the next
 * render, exactly as the movement badge does; the red override is flight-only
 * and hands over to loss-settle, which fades it out to reveal the green base.
 */

.ladder-row.is-result {
  z-index: 5;
  box-shadow:
    inset 2px 0 0 var(--lp-up),
    0 0 0 1px rgba(91, 214, 160, 0.38),
    0 14px 35px rgba(0, 0, 0, 0.4);
  background-color: #12231e;
}

.result-pop {
  position: absolute;
  right: 14px;
  top: -12px;
  color: var(--lp-up);
  background: #11251f;
  border: 1px solid rgba(91, 214, 160, 0.38);
  padding: 7px 12px;
  border-radius: 6px;
  font: 12px var(--numeric);
  z-index: 8;
  pointer-events: none;
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.33);
}

.result-pop.loss-pop {
  color: var(--lp-down);
  background: #28171f;
  border-color: rgba(224, 99, 127, 0.38);
}

.ladder-row.is-loss-result {
  background-color: #26171f;
  box-shadow:
    inset 2px 0 0 var(--lp-down),
    0 0 0 1px rgba(224, 99, 127, 0.31),
    0 12px 30px rgba(0, 0, 0, 0.33);
}

.ladder-row.is-loss-result .lp-pill {
  color: var(--lp-down);
  border-color: rgba(224, 99, 127, 0.38);
  background: rgba(224, 99, 127, 0.06);
}

.ladder-row.is-loss-landed {
  animation: loss-settle 1400ms ease-out;
}

@keyframes loss-settle {
  from {
    box-shadow:
      inset 2px 0 0 var(--lp-down),
      0 0 0 1px rgba(224, 99, 127, 0.31);
    background-color: #26171f;
  }
  to {
    box-shadow: inset 2px 0 0 rgba(224, 99, 127, 0);
    background-color: transparent;
  }
}



/* Human verification.
   Solved on load, then folded away. Collapsed with max-height rather than
   display:none so the iframe keeps running -- it has to stay alive to refresh
   the token when it expires, and it un-hides itself if it ever needs a human. */
.signup-turnstile {
  overflow: hidden;
  max-height: 80px;
  transition: max-height 220ms ease, opacity 220ms ease;
}

.signup-turnstile.is-verified {
  max-height: 0;
  opacity: 0;
}

/* Replaces the box with one quiet line on the label, so the check is visibly
   done rather than silently missing. The badge lives only here: an error
   paragraph below must never wear it -- a verified member reading "· verified
We can't verify…" is exactly the doubled, lying markup production showed. */
.signup-form:has(.signup-turnstile.is-verified) .signup-label::after {
  content: " · verified";
  color: #5bd6a0;
  font-size: 0.85em;
  letter-spacing: 0.04em;
}

/*
 * Riot's required fan-content notice.
 *
 * The shared footer treatment lives in style.css. The only thing the ladder
 * adds is stacking: .ladder-scene is fixed at z-index 0 and paints across
 * the whole viewport, so without its own stacking position this footer
 * renders behind the stage glow and shade, and is invisible.
 */
footer.legal {
  position: relative;
  z-index: 1;
}

/* Event feed: editorial weight, prominent banners vs quiet routine items */
.event-feed__event[data-priority="headline"] {
  border-left-width: 4px;
  background: rgba(18, 24, 38, 0.9);
  box-shadow: 0 0 16px color-mix(in srgb, currentColor 22%, transparent), inset 0 1px rgba(255, 255, 255, 0.08);
}

.event-feed__event[data-priority="headline"] .event-feed__actor {
  font-size: 0.92rem;
  font-weight: 800;
  letter-spacing: -0.02em;
}

.event-feed__event[data-priority="standard"] {
  opacity: 0.88;
  background: rgba(13, 17, 26, 0.45);
  border-color: rgba(255, 255, 255, 0.06);
  transition: opacity 180ms ease;
}

.event-feed__event[data-priority="standard"]:hover,
.event-feed__event[data-priority="standard"]:focus {
  opacity: 1;
}

.event-feed__time {
  margin-left: 6px;
  font-family: var(--numeric);
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  opacity: 0.75;
}

.event-feed__event[data-priority="headline"] .event-feed__time {
  opacity: 0.92;
  color: color-mix(in srgb, var(--text) 70%, transparent);
}

.event-feed__event--demotion {
  color: color-mix(in srgb, var(--lp-down) 75%, var(--muted));
}

.event-feed__event--lp-swing {
  color: var(--lp-up);
}

.event-feed__event--lp-drop {
  color: var(--lp-down);
}

.event-history__item--demotion,
.event-history__item--tier-demotion,
.event-history__item--rank-demotion {
  color: color-mix(in srgb, var(--lp-down) 75%, var(--muted));
}

.event-history__item--lp-swing {
  color: var(--lp-up);
}

.event-history__item--lp-drop {
  color: var(--lp-down);
}

@media (prefers-reduced-motion: reduce) {
  .event-feed__event[data-priority="standard"] {
    transition: none;
  }
}

/* ==========================================================================
   Event Info Dialog & Registration Surface Selectors
   ========================================================================== */

body:has(.ladder-scene) main {
  position: relative;
  z-index: 1;
}

/* The event info list, rendered inside the dialog. */
.signup-rules {
  margin: 0 0 20px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--panel);
  text-align: left;
  font-family: var(--font-display);
  letter-spacing: normal;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}

.signup-rules-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.signup-rules-item {
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--muted);
}

.signup-rules-topic {
  color: var(--text);
  font-weight: 600;
}

.signup-rules-detail {
  color: var(--muted);
}

/* --------------------------------------------------------- info dialog */

/*
 * A real <dialog> opened with showModal(), matching the clips page's dialogs
 * rather than being bespoke: same zeroed padding/border with the panel inside,
 * same backdrop treatment. Backdrop clicks reach the dialog element itself,
 * which is how js/ladder/rules.js detects them.
 */
dialog.rules-dialog {
  padding: 0;
  border: 0;
  background: transparent;
  max-width: min(680px, calc(100vw - 48px));
  max-height: calc(100dvh - 48px);
  width: 100%;
  color: var(--text);
}

dialog.rules-dialog::backdrop {
  background: rgba(3, 6, 12, 0.85);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}

.rules-dialog__panel {
  display: grid;
  gap: var(--s-4, 16px);
  padding: 32px 36px;
  border: 1px solid var(--gold-dim);
  border-radius: var(--r-lg);
  background: #12161f;
  box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
  color: var(--text);
  box-sizing: border-box;
  /* The panel scrolls internally: a growing list must never push the dialog
     past the viewport, and the dialog itself never pages the document. */
  overflow-y: auto;
  max-height: calc(100dvh - 48px);
}

.rules-dialog__title {
  margin: 0;
  color: var(--text);
  font-family: var(--font-display);
  font-size: 1.45rem;
  font-weight: 700;
  letter-spacing: 0.04em;
}

/*
 * Inside the dialog the info is the main event, not fine print flanking a
 * form: it steps up to a full reading size. Scoped to the dialog so the
 * inline registration copy keeps its compact scale if it ever returns.
 */
.rules-dialog__panel .signup-rules-item {
  font-size: 1rem;
  line-height: 1.6;
}

/* The list scrolls only when a short window demands it; the cap is tall
   enough that every line fits whenever the viewport allows. */
.rules-dialog__panel .signup-rules {
  margin: 0;
  padding: 20px 24px;
  max-height: min(62dvh, 520px);
  overflow-y: auto;
}

.rules-dialog__actions {
  display: flex;
  justify-content: flex-end;
}

/*
 * OK is the primary action, so it takes the gold treatment the signup submit
 * already owns — one idea of "the button that moves you forward" per page.
 */
.rules-dialog__ok {
  flex: 0 0 auto;
  padding: 12px 28px;
  border: 1px solid #d8ba6b;
  border-radius: 8px;
  background: rgba(216, 186, 107, 0.1);
  color: #d8ba6b;
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  cursor: pointer;
  transition: background 200ms ease, color 200ms ease;
}

.rules-dialog__ok:hover {
  background: #d8ba6b;
  color: #171207;
}

.rules-dialog__ok:focus-visible {
  outline: 2px solid var(--crown-gold);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .rules-dialog__ok,
  .rules-dialog__close {
    transition: none;
  }
}

.registration-dialog__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--s-2, 8px);
}

.rules-dialog__close {
  background: none;
  border: 0;
  color: var(--muted);
  font-size: 1.2rem;
  line-height: 1;
  padding: 4px 8px;
  cursor: pointer;
  border-radius: var(--r-sm);
  transition: color 150ms ease;
}

.rules-dialog__close:hover {
  color: var(--text);
}



/*
 * Laptop widths are where the row is tightest, and its fixed tracks were
 * sized for the wide board. What yields, in order: the rank badge + LP pair
 * (context -- the player's solo-queue rank, not event data; it stays on the
 * player page), the form strip's ten ticks (the streak chip keeps the story,
 * and the strip already disappears below 900px), the fifth champion, and the
 * stat tracks' air. The name is the one thing every row must show, so it
 * keeps the room: a typical Riot ID fits in full at 1440, and only genuinely
 * long names truncate.
 *
 * This lives at the end of the sheet because it overrides declarations whose
 * base rules sit further down (the form strip, the streak chip); a media
 * query gains no cascade weight from its condition, only from its position.
 * The min-width guard keeps every narrower breakpoint's own layout winning.
 */
@media (max-width: 1649px) and (min-width: 901px) {
  .ladder-head,
  .ladder-row {
    grid-template-columns:
      minmax(240px, 1fr)
      /* Four champions at 34px plus three 5px gaps. */
      152px
      72px
      58px
      58px
      54px
      58px;
    gap: 11px;
    padding-inline: 18px;
  }

  .champion-icon:nth-child(n + 5) { display: none; }

  .rank-badge,
  .rank-lp,
  .form-strip {
    display: none;
  }

  /* The fixed badge/LP tracks they lived in collapse with them. */
  .player-line {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .col-player { padding-left: 76px; }

  /* The roster variant stays single-column: it has no live cells to align. */
  .ladder-row--roster {
    grid-template-columns: minmax(240px, 1fr);
  }
}

@media (max-width: 1280px) and (min-width: 901px) {
  /* The last elastic piece before the name itself starves: without this the
     live chip plus streak chip leave a 1280px row no name track at all. */
  .streak-chip { display: none; }
}
