/* Shared components.
 *
 * Class names are taken from what the templates already use, so a page can move
 * onto this stylesheet by deleting its <style> block rather than by rewriting
 * its markup -- which matters, because that markup is addressed by id from the
 * script block in the same file.
 *
 * Where the pages had drifted into several names for one thing, all were kept
 * as aliases during the restyle and converged afterwards in commits of their
 * own, so a rename never rode along with a visual change. Both are done:
 * pill / nav-pill is now just .pill, and .btn / .btn-sm / .btn-primary /
 * .btn-green is now .button with modifiers.
 *
 * Classes the scripts select on are deliberately NOT part of that tidying --
 * .sync-now, .reconnect and .outcome-choice are behavioural hooks, and
 * admin_console assigns .badge-red from JS. Renaming those breaks behaviour
 * silently, which is exactly what no test would catch. */

/* ---- surfaces ---- */

.panel {
  padding: var(--pad-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--surface-sheen), var(--shadow-sm);
}

.card {
  display: grid;
  gap: var(--sp-2);
  align-content: start;
  padding: var(--pad-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  box-shadow: var(--surface-sheen);
}

.card h2,
.card h3 { margin-bottom: 0; }

.card-sunken {
  padding: var(--pad-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}

/* The opening panel's finish, kept separate from its layout.
 *
 * .hero is a layout as well as a skin -- grid, justify-items: start -- and that
 * layout is wrong for an opening section that holds a full-width grid of cards
 * inside it, which is exactly why home.html was a plain .panel and never got
 * the treatment. .hero-panel is the same finish with no layout attached, so the
 * landing page can wear it over the panel it already is.
 *
 * Two washes rather than one, from opposite corners: teal off the leading edge
 * where the headline sits, and a much fainter gold off the trailing one. A
 * single corner blob reads as a stray gradient; two give the panel a direction
 * across it. The teal is mixed here rather than taken from --accent-soft
 * because a panel this size needs more of it than a badge fill does before the
 * colour is visible at all. */
.hero,
.panel.hero-panel {
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(72% 88% at 0% 0%,
                    color-mix(in srgb, var(--accent-bright) 12%, transparent),
                    transparent 64%),
    radial-gradient(58% 74% at 100% 0%,
                    color-mix(in srgb, var(--emphasis) 7%, transparent),
                    transparent 62%),
    var(--surface);
  box-shadow: var(--surface-sheen), var(--shadow-md);
}

/* The accent rule along the top edge. Same device as the featured pricing card,
 * deliberately: used twice it reads as the system marking what matters, used
 * once it would read as decoration on the landing page. */
.hero::before,
.panel.hero-panel::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--accent-rule);
}

.hero {
  display: grid;
  gap: var(--sp-4);
  justify-items: start;
  padding: var(--sp-7) var(--pad-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}

.hero h1 { max-width: 20ch; margin-bottom: 0; }
.hero .eyebrow { margin-bottom: 0; }

/* ---- actions ---- */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  min-height: 44px;
  padding: var(--sp-3) var(--sp-5);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  /* Gradient rather than flat fill, and a shadow tinted with the fill's own
   * hue rather than black. Both are what separate a control that looks placed
   * on the page from one that looks printed onto it. */
  background: var(--accent-gradient);
  box-shadow: var(--accent-glow), var(--surface-sheen);
  color: var(--accent-ink);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition: box-shadow 160ms ease, border-color 120ms ease,
              transform 120ms ease, filter 160ms ease;
}

/* The fill is a gradient, so hover cannot swap a single colour. Lifting the
 * whole control and brightening it in place is the same signal and survives
 * every variant below, including the ones that replace the background. */
.button:hover { filter: brightness(1.07) saturate(1.06); text-decoration: none; }
.button:not([disabled]):not([aria-disabled="true"]):hover { transform: translateY(-1px); }
.button:active { transform: translateY(0); filter: brightness(0.97); }

.button[disabled],
.button[aria-disabled="true"] {
  opacity: 0.5;
  box-shadow: none;
  cursor: not-allowed;
  filter: none;
  transform: none;
}

.button.secondary,
.button-secondary {
  background: transparent;
  border-color: var(--line-strong);
  box-shadow: none;
  color: var(--ink);
}
.button.secondary:hover,
.button-secondary:hover { background: var(--surface-sunken); }

.button.ghost {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
  color: var(--accent);
}
.button.ghost:hover { background: var(--accent-soft); }

/* Not #fff. --positive and --negative are dark fills in light mode but light
 * fills in dark mode, so a hardcoded white label drops to about 2:1 on a dark
 * theme. --ink-inverse flips with the theme and is legible against both. */
.button.danger { background: var(--negative); box-shadow: var(--surface-sheen); color: var(--ink-inverse); }
.button.positive { background: var(--positive); box-shadow: var(--surface-sheen); color: var(--ink-inverse); }

/* base.css draws the focus ring with a zero-specificity :where() selector, so
 * any component that sets its own box-shadow silently wins the cascade and the
 * ring never appears. Now that the primary control carries a glow, it has to
 * restate the ring itself -- and last, so the variants above cannot drop it. */
.button:focus-visible { box-shadow: var(--focus-ring); }

/* The operator console packs buttons into table rows, which is what the old
 * .btn / .btn-sm pair existed for. It is a size, not a separate control, so it
 * is a modifier here. The two old sizes differed by 1px of padding and 0.03rem
 * of type; collapsing them to one loses nothing anyone could see.
 *
 * 44px touch targets are a customer-UI commitment, and this deliberately opts
 * out of it: admin is operator-only, mouse-driven, and dense on purpose. */
.button.compact {
  min-height: 30px;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--radius-sm);
  font-size: var(--text-xs);
}

.button.full { width: 100%; }

/* A pill-shaped chip. Mostly navigation, but not only: the dashboard uses one on
 * a <span> for the signed-in email, which is why this is .pill and not .nav-pill.
 * The nav-pill alias is gone -- every page says .pill now. */
.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: 38px;
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--text-sm);
  font-weight: 500;
}

.pill:hover { border-color: var(--line-strong); color: var(--ink); text-decoration: none; }

/* The signed-in address is a status pill, not a control -- the only .pill with
 * no href. A real address wraps to its own full-width row on a phone, and with
 * the brand and the nav above it that pushed "What should I do right now?"
 * under the fold on a 375px screen. Truncating keeps the which-account signal,
 * which matters on a shared phone, without spending a row on it. */
@media (max-width: 719px) {
  .nav .pill:not([href]) {
    display: inline-block;
    max-width: 11rem;
    overflow: hidden;
    line-height: 22px;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

.pill[aria-current="page"] {
  border-color: transparent;
  background: var(--accent-soft);
  color: var(--accent);
}

/* ---- form fields ---- */

.field { display: flex; flex-direction: column; gap: var(--sp-1); }

/* input:not([type]) is load-bearing, not defensive. An <input> with no type
 * attribute behaves as text but does not match input[type="text"] -- the
 * selector matches the attribute, not the default. Twelve fields across the
 * dashboard, setup and account pages are written that way (most carry
 * inputmode="numeric" instead), and without this they fall back to the UA's
 * 13.33px. Anything under 16px makes iOS Safari zoom the page in when the field
 * is focused, and it does not zoom back out, so the league-id field on the
 * dashboard would leave a phone user pinching to escape the form. */
input:not([type]),
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="tel"],
input[type="url"],
select,
textarea {
  width: 100%;
  min-height: 44px;
  padding: var(--sp-3);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--text-base);
}

textarea { min-height: 96px; resize: vertical; line-height: var(--leading-snug); }

input::placeholder, textarea::placeholder { color: var(--ink-subtle); }

input:disabled, select:disabled, textarea:disabled {
  background: var(--surface-sunken);
  color: var(--ink-subtle);
  cursor: not-allowed;
}

.field-hint {
  color: var(--ink-subtle);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

.field-error { color: var(--negative); font-size: var(--text-sm); font-weight: 500; }

/* ---- data ---- */

/* .value is the readable answer under a .label -- an email address, a plan
 * name, "Available". It must wrap, so it is not the mono numeric treatment.
 * Big numeric readouts are .metric. */
.value {
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: var(--leading-snug);
  overflow-wrap: anywhere;
}

.metric {
  font-family: var(--font-mono);
  font-size: var(--text-2xl);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-tight);
  line-height: 1.1;
}

.table-wrap { overflow-x: auto; }

table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
}

th, td {
  padding: var(--sp-3);
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
}

th {
  color: var(--ink-subtle);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  white-space: nowrap;
}

tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--surface-sunken); }

td.num, th.num { text-align: right; font-family: var(--font-mono); }

/* ---- status ---- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
}

.badge.positive { background: color-mix(in srgb, var(--positive) 16%, transparent); color: var(--positive); }
.badge.negative { background: color-mix(in srgb, var(--negative) 16%, transparent); color: var(--negative); }
.badge.caution  { background: color-mix(in srgb, var(--caution) 18%, transparent); color: var(--caution); }
.badge.accent   { background: var(--accent-soft); color: var(--accent); }

.notice {
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  font-size: var(--text-sm);
}

.notice.positive { border-left-color: var(--positive); }
.notice.negative { border-left-color: var(--negative); }
.notice.caution  { border-left-color: var(--caution); }

.empty-state {
  padding: var(--sp-7) var(--sp-5);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-md);
  color: var(--ink-subtle);
  text-align: center;
}

/* ---- fact list: a term and its plain-language explanation, used wherever the
 * product has to state a commitment rather than describe a feature ---- */

.truth-list {
  display: grid;
  gap: var(--sp-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.truth-list li {
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

.truth-list strong {
  display: block;
  margin-bottom: var(--sp-1);
  color: var(--ink);
  font-weight: 600;
}

.truth-list span { color: var(--ink-muted); }

.warning {
  border-color: color-mix(in srgb, var(--negative) 32%, transparent);
  background: color-mix(in srgb, var(--negative) 7%, var(--surface));
}

/* toolbar is a row of actions; kept as its own name because the markup uses it. */
.toolbar {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

/* ---- the Weekly Answer cards.
 * Shared by the landing page's demo and the real dashboard, so this lives in
 * the system rather than in either page. ---- */

.grid-6 { display: grid; gap: var(--sp-3); grid-template-columns: 1fr; }

@media (min-width: 620px) { .grid-6 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 980px) { .grid-6 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.answer-card {
  display: grid;
  gap: var(--sp-2);
  align-content: start;
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  box-shadow: var(--surface-sheen);
}

.answer-card span {
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.answer-card strong {
  display: block;
  color: var(--ink);
  font-size: var(--text-base);
  font-weight: 600;
  line-height: var(--leading-snug);
}

/* The dashboard's cards carry a detail line under the headline; the landing
 * page's do not. */
.answer-card p { margin: 0; color: var(--ink-muted); font-size: var(--text-sm); line-height: var(--leading-snug); }

/* The top move's case, one point per line. Prose hid the category trade -- what the
 * move wins you and what it costs you -- inside a paragraph the customer had to parse
 * before they could act. Markers are drawn with a border rather than list-style so they
 * stay aligned with the text at any font size. */
.answer-rationale {
  display: grid;
  gap: var(--sp-1);
  margin: 0;
  padding: 0;
  list-style: none;
}

.answer-rationale li {
  position: relative;
  padding-left: var(--sp-3);
  color: var(--ink-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

.answer-rationale li::before {
  content: "";
  position: absolute;
  top: 0.55em;
  left: 0;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--ink-subtle);
}

/* ---- the Weekly Answer surface.
 * data-answer-state is set by the dashboard script and is the product's honesty
 * mechanism: stale, locked, unavailable, error and missing-data must all read
 * as degraded at a glance, never as a clean answer. ---- */

.answer-split { display: grid; gap: var(--sp-4); align-items: stretch; grid-template-columns: 1fr; }

@media (min-width: 720px) {
  .answer-split { grid-template-columns: minmax(0, 0.95fr) minmax(340px, 1.05fr); }
}
@media (min-width: 1080px) {
  .answer-split { grid-template-columns: minmax(0, 0.95fr) minmax(400px, 1.05fr); }
}

.answer-shell { display: grid; gap: var(--sp-4); align-content: start; }

.answer-status { display: flex; justify-content: space-between; align-items: flex-start; gap: var(--sp-3); }
.answer-status h2 { margin-bottom: 0; }
.answer-status .eyebrow { margin-bottom: var(--sp-1); }

.status-badge {
  flex: none;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--ink-muted);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.answer-shell[data-answer-state="fresh"] .status-badge {
  background: color-mix(in srgb, var(--positive) 16%, transparent);
  color: var(--positive);
}

.answer-shell[data-answer-state="saved"] .status-badge {
  background: color-mix(in srgb, var(--caution) 18%, transparent);
  color: var(--caution);
}

.answer-shell[data-answer-state="stale"],
.answer-shell[data-answer-state="locked"],
.answer-shell[data-answer-state="unavailable"],
.answer-shell[data-answer-state="error"],
.answer-shell[data-answer-state="missing-data"] {
  border-color: color-mix(in srgb, var(--negative) 42%, transparent);
}

.answer-shell[data-answer-state="stale"] .status-badge,
.answer-shell[data-answer-state="locked"] .status-badge,
.answer-shell[data-answer-state="unavailable"] .status-badge,
.answer-shell[data-answer-state="error"] .status-badge,
.answer-shell[data-answer-state="missing-data"] .status-badge {
  background: color-mix(in srgb, var(--negative) 16%, transparent);
  color: var(--negative);
}

.answer-grid { display: grid; gap: var(--sp-3); grid-template-columns: 1fr; }

@media (min-width: 720px) {
  .answer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .warning-card { grid-column: 1 / -1; }
}

.warning-card {
  border-color: color-mix(in srgb, var(--caution) 40%, transparent);
  background: color-mix(in srgb, var(--caution) 10%, var(--surface));
}

/* ---- dashboard controls ---- */

.setup-strip { display: grid; gap: var(--sp-3); margin-top: var(--sp-4); }
.setup-strip label,
.mini-grid label { display: grid; gap: var(--sp-1); color: var(--ink-muted); font-size: var(--text-sm); font-weight: 600; }

.action-row { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.action-row button, .action-row .button { flex: 1 1 auto; }

@media (min-width: 720px) {
  .action-row button, .action-row .button { flex: 0 0 auto; }
}

.mini-grid { display: grid; gap: var(--sp-2); grid-template-columns: 1fr; }

@media (min-width: 720px) { .mini-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.transaction-card {
  padding: var(--sp-4);
  border: 1px solid color-mix(in srgb, var(--caution) 34%, transparent);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--caution) 7%, var(--surface));
}

.transaction-card summary { cursor: pointer; font-weight: 600; }
.transaction-card > *:not(summary) { margin-top: var(--sp-3); }

.diagnostics { margin-top: var(--sp-4); }
.diagnostics summary { cursor: pointer; }

.diagnostics pre {
  max-height: 360px;
  margin: var(--sp-3) 0 0;
  padding: var(--sp-4);
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--n-950);
  color: var(--n-100);
  font-size: var(--text-sm);
}

/* ---- weekly outcome feedback ---- */

.outcome-prompt {
  display: none;
  margin-top: var(--sp-5);
  padding: var(--pad-panel);
  border: 1px solid color-mix(in srgb, var(--caution) 34%, transparent);
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--caution) 8%, var(--surface));
}

.outcome-prompt[data-visible="true"] { display: block; }
.outcome-prompt h3 { margin-bottom: var(--sp-2); }
.outcome-prompt > p { margin: 0 0 var(--sp-4); color: var(--ink-muted); }

.outcome-choices { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-bottom: var(--sp-3); }

.outcome-choice {
  min-height: 44px;
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--ink);
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
}

.outcome-choice:hover { border-color: var(--accent); }

.outcome-choice[aria-pressed="true"] {
  border-color: transparent;
  background: var(--accent);
  color: var(--accent-ink);
}

.outcome-note-field { display: none; margin-bottom: var(--sp-3); }
.outcome-note-field[data-visible="true"] { display: block; }

/* ---- setup: extension store listings ---- */

.store-buttons { display: grid; gap: var(--sp-3); margin-top: var(--sp-4); grid-template-columns: 1fr; }
.helper-grid { display: grid; gap: var(--sp-2); margin-top: var(--sp-3); grid-template-columns: 1fr; }

@media (min-width: 720px) {
  .store-buttons { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .helper-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.store-button {
  display: grid;
  gap: var(--sp-1);
  justify-items: center;
  min-height: 44px;
  padding: var(--sp-5) var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--ink);
  font-size: var(--text-lg);
  font-weight: 600;
  text-align: center;
}

.store-button span { color: var(--ink-muted); font-size: var(--text-sm); font-weight: 400; }

/* Unpublished listing: same shape, visibly inert, and never a link. */
.store-button.unpublished {
  border-style: dashed;
  background: var(--surface-sunken);
  color: var(--ink-subtle);
  cursor: default;
}

.store-fallback { margin-top: var(--sp-3); font-size: var(--text-sm); }

/* ---- setup: the bookmarklet drag target.
 * Looks like a button but must stay an anchor -- dragging it to the bookmarks
 * bar is what installs it. ---- */

.bookmarklet-button {
  display: inline-block;
  min-height: 44px;
  padding: var(--sp-3) var(--sp-6);
  border: 2px solid var(--ink);
  border-radius: var(--radius-md);
  background: var(--ink);
  color: var(--ink-inverse);
  font-size: var(--text-lg);
  font-weight: 600;
  text-decoration: none;
  cursor: grab;
}

.bookmarklet-button:active { cursor: grabbing; }
.bookmarklet-button:hover { text-decoration: none; }

/* Insecure origin: the bookmarklet is never generated, so the drag target reads
 * as unavailable rather than handing over a link that cannot be used safely. */
.bookmarklet-button[aria-disabled="true"] {
  border-style: dashed;
  border-color: var(--line-strong);
  background: var(--surface-sunken);
  color: var(--ink-subtle);
  cursor: not-allowed;
}

.bookmarklet-row { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; margin: var(--sp-4) 0 var(--sp-2); }

/* ---- setup: flow banners ---- */

.flow-banner {
  display: grid;
  gap: var(--sp-1);
  margin-top: var(--sp-4);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid color-mix(in srgb, var(--accent) 26%, transparent);
  border-radius: var(--radius-md);
  background: var(--accent-soft);
  color: var(--ink);
}

.flow-banner strong { font-size: var(--text-base); }
.flow-banner span { color: var(--ink-muted); font-size: var(--text-sm); line-height: var(--leading-snug); }

/* Warning variant for the insecure-origin notice. */
.flow-banner.insecure {
  border-color: color-mix(in srgb, var(--negative) 38%, transparent);
  background: color-mix(in srgb, var(--negative) 9%, var(--surface));
}

/* The phone note. Warm rather than red on purpose: needing a computer once is
 * a detour, not a failure, and the insecure-origin banner directly above it is
 * the one that genuinely blocks a connect. */
.flow-banner.mobile-note {
  border-color: color-mix(in srgb, var(--emphasis) 34%, transparent);
  background: color-mix(in srgb, var(--emphasis) 9%, var(--surface));
}

ol.setup-steps { display: grid; gap: var(--sp-2); margin: var(--sp-4) 0 0; padding: 0 0 0 var(--sp-5); }
ol.setup-steps li { line-height: var(--leading-snug); }
ol.setup-steps strong { display: block; }
ol.setup-steps span { color: var(--ink-muted); font-size: var(--text-sm); }

/* ---- setup: connected leagues ---- */

.league-list { display: grid; gap: var(--sp-3); margin-top: var(--sp-4); }

.league-row {
  display: grid;
  gap: var(--sp-2);
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  box-shadow: var(--surface-sheen), var(--shadow-sm);
}

.league-row-head { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.league-row-head strong { font-size: var(--text-lg); }
.league-meta { color: var(--ink-muted); font-size: var(--text-sm); line-height: var(--leading-snug); }
.league-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
.league-actions button { flex: 1 1 auto; }

@media (min-width: 720px) { .league-actions button { flex: 0 0 auto; } }

/* Whether stored ESPN credentials are encrypted is a trust claim, so it reads as
 * positive or negative rather than as neutral chrome. */
.badge.encrypted { background: color-mix(in srgb, var(--positive) 16%, transparent); color: var(--positive); }
.badge.plaintext { background: color-mix(in srgb, var(--negative) 16%, transparent); color: var(--negative); }

/* ---- setup: advanced fallback ---- */

details.advanced {
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}

details.advanced summary {
  display: flex;
  align-items: center;
  min-height: 44px;
  list-style: none;
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 600;
  cursor: pointer;
}

details.advanced summary::-webkit-details-marker { display: none; }
details.advanced[open] summary { margin-bottom: var(--sp-2); }

.helper-panel {
  display: grid;
  gap: var(--sp-1);
  padding: var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.helper-panel strong { font-size: var(--text-sm); }

.cookie-paste-helper {
  display: grid;
  gap: var(--sp-2);
  padding: var(--sp-3);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.cookie-helper-actions { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }

/* ---- operator console and ops.
 *
 * Admin used to carry its own button vocabulary (.btn / .btn-sm / .btn-primary
 * / .btn-green). It is gone: operator density is a size modifier on the one
 * control, .button.compact, not a second control. Admin stays denser than the
 * customer UI, but through the same component. */
button:disabled { cursor: not-allowed; opacity: 0.6; }

.badge-green { background: color-mix(in srgb, var(--positive) 16%, transparent); color: var(--positive); }
.badge-red { background: color-mix(in srgb, var(--negative) 16%, transparent); color: var(--negative); }
.badge-gray { background: var(--surface-sunken); color: var(--ink-muted); }
.badge-row { display: flex; gap: var(--sp-2); flex-wrap: wrap; }

/* .actions is a spaced button row on customer pages, but the console uses it as
 * a table-cell modifier. Keep the cell from inheriting flex and a top margin. */
td.actions { display: table-cell; margin: 0; white-space: nowrap; }

/* Used both as a heading row (h2 + a filter form) and as the form itself, so the
 * two are pinned to their content size. Left to flex defaults the heading claims
 * the row and the form shrinks to its input, pushing the submit button onto a
 * line of its own. */
.filter-bar {
  display: flex;
  gap: var(--sp-2);
  align-items: center;
  flex-wrap: wrap;
  justify-content: space-between;
}

.filter-bar > h2 { flex: 0 0 auto; margin-bottom: 0; }
.filter-bar > form { flex: 0 0 auto; justify-content: flex-end; }
/* Not min(260px, 100%): inside a flex form that percentage resolves against a
 * width the input itself determines, and the button gets squeezed onto its own
 * line. A fixed width with a max-width cap breaks the circularity. */
.filter-bar input { flex: 0 0 auto; width: 260px; max-width: 100%; }
.card > .filter-bar { margin-bottom: var(--sp-3); }

.pagination { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; }

.pagination a, .pagination span {
  display: inline-block;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 500;
}

.pagination .current { background: var(--accent); color: var(--accent-ink); }

.status-grid, .advanced-grid { display: grid; gap: var(--sp-4); grid-template-columns: 1fr; }

@media (min-width: 861px) {
  .status-grid, .advanced-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.internal-grid { display: grid; gap: var(--sp-3); margin-top: var(--sp-4); }
.raw-block { display: grid; gap: var(--sp-2); }

.internal-drawer {
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
}

.internal-drawer summary {
  list-style: none;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  cursor: pointer;
}

.internal-drawer summary::-webkit-details-marker { display: none; }
.internal-drawer[open] summary { margin-bottom: var(--sp-2); }

.summary-label {
  display: block;
  color: var(--ink-subtle);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.operator-note, .section-copy {
  max-width: 76ch;
  color: var(--ink-muted);
  line-height: var(--leading-body);
}

.section-copy { margin-top: var(--sp-1); }

/* Raw payload dumps. Always dark, in both themes: operators read these as
 * terminal output and a light-on-light JSON blob is harder to scan. */
pre.json-block, .raw-block pre, .internal-drawer pre {
  max-height: 360px;
  margin: var(--sp-2) 0 0;
  padding: var(--sp-4);
  overflow: auto;
  border-radius: var(--radius-sm);
  background: var(--n-950);
  color: var(--n-100);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  white-space: pre-wrap;
}

/* ---- modal ---- */

.overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(0, 0, 0, 0.45);
  place-items: center;
}

.overlay.open { display: grid; }

.modal {
  display: grid;
  gap: var(--sp-3);
  width: min(440px, calc(100vw - 32px));
  padding: var(--sp-6);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--surface-sheen), var(--shadow-lg);
}

.modal h2 { margin: 0; }
.modal label { font-size: var(--text-sm); font-weight: 600; }
.btn-row { display: flex; gap: var(--sp-2); justify-content: flex-end; }

/* ---- operator login ---- */

.error { color: var(--negative); font-weight: 500; }

/* ---- sticky mobile action bar ---- */

.mobile-action-bar { display: none; }

@media (max-width: 719px) {
  /* Clears the bar itself plus whatever the device reserves below it, so the
     last card on the page is never trapped under the bar. */
  body { padding-bottom: calc(72px + env(safe-area-inset-bottom)); }

  .mobile-action-bar {
    display: flex;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 100;
    gap: var(--sp-2);
    /* Installed to a home screen there is no browser toolbar underneath this
       bar, so on a notched phone it would otherwise sit on top of the home
       indicator -- the buttons look right but swallow the swipe. The extra
       bottom padding lifts the tap targets clear while the background still
       bleeds to the edge. */
    padding: var(--sp-3) max(var(--sp-4), env(safe-area-inset-right))
             calc(var(--sp-3) + env(safe-area-inset-bottom))
             max(var(--sp-4), env(safe-area-inset-left));
    border-top: 1px solid var(--line);
    background: var(--surface);
    box-shadow: var(--shadow-lg);
  }

  .mobile-action-bar button { flex: 1 1 0; }

  /* The inline row would duplicate the sticky bar's actions. */
  .setup-strip .action-row { display: none; }
}

.demo-label {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--radius-pill);
  background: var(--emphasis-soft);
  color: var(--emphasis);
  font-size: var(--text-xs);
  font-weight: 600;
}

/* The demo now loads on its own instead of waiting for a click, so it needs a
 * line saying what it is before the reader decides whether these six cards are
 * their league. Sits between the badge and the grid. */
/* A clear break between the pitch and the proof -- the badge is small and sat
 * almost against the button above it. */
.demo-result { margin-top: var(--sp-6); }
.demo-caption { margin: var(--sp-2) 0 var(--sp-4); max-width: 62ch; }
#demo-retry { margin-top: var(--sp-4); }

.espn-pill {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-2) var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/* ---- inline status line.
 * The scripts set el.dataset.tone, so these selectors are load-bearing: drop
 * them and every success and failure message renders as plain grey text. ---- */

.message { margin: 0; font-size: var(--text-sm); font-weight: 500; min-height: 1.4em; }
.message[data-tone="ok"] { color: var(--positive); }
.message[data-tone="error"] { color: var(--negative); }

/* .status is the same idea with a different tone vocabulary: home says "ok",
 * account_settings says "success". Both are supported rather than renamed,
 * because converging them is a JS change and this is a restyle. */
.status {
  min-height: 1.5em;
  color: var(--ink-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

.status[data-tone="success"] { color: var(--positive); }
.status[data-tone="error"] { color: var(--negative); }

/* ---- account settings ---- */

.danger-note {
  border-color: color-mix(in srgb, var(--negative) 32%, transparent);
  background: color-mix(in srgb, var(--negative) 7%, var(--surface));
}

/* Hidden until the script sets data-visible, which is how the rotated key is
 * revealed exactly once. */
.api-key-box { display: none; }

.api-key-box[data-visible="true"] {
  display: grid;
  gap: var(--sp-2);
  padding: var(--sp-4);
  border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
  border-radius: var(--radius-md);
  background: var(--accent-soft);
}

.api-key-box code {
  display: block;
  padding: var(--sp-3);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* ---- marketing blocks ---- */

.actions { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-top: var(--sp-5); }
.actions + .fine-print { margin-top: var(--sp-3); }

/* On a phone the two hero buttons wrap to their own rows anyway, but sized to
 * their text they land at two different widths, which reads as ragged rather
 * than deliberate. Full width squares them off and gives the primary action the
 * whole thumb. Content width takes over from the first breakpoint up. */
@media (max-width: 619px) {
  .actions { flex-direction: column; align-items: stretch; }
  .actions .button { width: 100%; }
}

.section-gap { margin-top: var(--sp-7); }
.section-inner { margin-top: var(--sp-5); }

.step-card {
  display: grid;
  gap: var(--sp-2);
  align-content: start;
  padding: var(--pad-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: var(--surface-sheen), var(--shadow-sm);
}

.step-card h3 { margin-bottom: 0; }
.step-card p { color: var(--ink-muted); font-size: var(--text-sm); }

.step-num {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: var(--radius-pill);
  background: var(--accent-gradient);
  box-shadow: var(--accent-glow);
  color: var(--accent-ink);
  font-size: var(--text-sm);
  font-weight: 700;
}

.trust-strip { display: grid; gap: var(--sp-3); }

@media (min-width: 720px) { .trust-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.trust-item {
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent-bright);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--ink-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

.trust-item strong { display: block; margin-bottom: var(--sp-1); color: var(--ink); }

.compare-wrapper { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.compare-table { min-width: 640px; }
.compare-table td:first-child { color: var(--ink-muted); font-weight: 600; }
.compare-table td.check { color: var(--positive); }
.compare-table td.cross { color: var(--ink-subtle); }

.beta-box {
  display: grid;
  gap: var(--sp-3);
  justify-items: center;
  padding: var(--sp-7) var(--pad-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background:
    radial-gradient(100% 140% at 50% 0%, var(--accent-soft), transparent 62%),
    var(--surface);
  box-shadow: var(--surface-sheen), var(--shadow-sm);
  text-align: center;
}

.beta-box h2 { margin-bottom: 0; }
.beta-form { display: flex; gap: var(--sp-2); flex-wrap: wrap; justify-content: center; width: min(100%, 460px); }
.beta-form input { flex: 1 1 220px; }

.img-placeholder {
  display: grid;
  place-items: center;
  min-height: 200px;
  padding: var(--sp-6);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-md);
  color: var(--ink-subtle);
  font-size: var(--text-sm);
  text-align: center;
}

.account-card { display: grid; gap: var(--sp-5); }
.account-card form { display: grid; gap: var(--sp-3); }
.account-card label { display: grid; gap: var(--sp-1); font-size: var(--text-sm); font-weight: 600; color: var(--ink-muted); }
.account-card summary { cursor: pointer; padding: var(--sp-2) 0; color: var(--accent); }

.footer {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  flex-wrap: wrap;
  margin-top: var(--sp-8);
  padding-top: var(--sp-5);
  border-top: 1px solid var(--line);
  color: var(--ink-subtle);
  font-size: var(--text-sm);
}

.footer a { font-weight: 500; }
.footer-spacer { flex: 1; }

/* ---- pricing ---- */

.pricing-card {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--pad-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.pricing-card.featured,
.plan-card-pro {
  position: relative;
  overflow: hidden;
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
  box-shadow: var(--surface-sheen), var(--shadow-md), var(--accent-glow);
}

.pricing-card.featured::before,
.plan-card-pro::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: var(--accent-rule);
}

.pricing-card ul { margin: 0; padding-left: var(--sp-5); color: var(--ink-muted); font-size: var(--text-sm); }
.pricing-card li { margin-bottom: var(--sp-2); }
.pricing-card strong { font-family: var(--font-display); font-size: var(--text-xl); font-weight: 600; }
.pricing-card .muted { font-size: var(--text-sm); line-height: var(--leading-snug); }

.plan-grid { display: grid; gap: var(--gap-stack); margin-top: var(--sp-5); grid-template-columns: 1fr; }
.pricing-grid { display: grid; gap: var(--sp-3); margin-top: var(--sp-4); grid-template-columns: 1fr; }

@media (min-width: 720px) {
  .plan-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .pricing-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1000px) { .pricing-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* Equal-height cards with unequal content: pin the CTA to the bottom rather
 * than spreading everything with space-between, which leaves the shorter plan
 * full of gaps. */
.plan-card > .plan-cta { margin-top: auto; }

.plan-price {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: 600;
  letter-spacing: var(--tracking-tight);
}

.plan-price span {
  margin-left: var(--sp-2);
  color: var(--ink-subtle);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 400;
  letter-spacing: 0;
}

.plan-list { margin: 0; padding-left: var(--sp-5); color: var(--ink-muted); font-size: var(--text-sm); }
.plan-list li { margin-bottom: var(--sp-2); }

.plan-cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: var(--sp-3) var(--sp-5);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--accent-gradient);
  box-shadow: var(--accent-glow), var(--surface-sheen);
  color: var(--accent-ink);
  font-size: var(--text-sm);
  font-weight: 600;
  text-align: center;
  transition: filter 160ms ease, transform 120ms ease;
}

.plan-cta:hover { filter: brightness(1.07) saturate(1.06); transform: translateY(-1px); text-decoration: none; }

.plan-cta-quiet {
  background: transparent;
  border-color: var(--line-strong);
  box-shadow: none;
  color: var(--ink);
}
.plan-cta-quiet:hover { background: var(--surface-sunken); filter: none; transform: none; }

.access-panel {
  display: grid;
  gap: var(--sp-2);
  justify-items: start;
  margin-top: var(--sp-6);
  padding: var(--pad-panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}

.access-panel h3 { margin-bottom: 0; }

.access-pill {
  display: inline-flex;
  align-items: center;
  padding: var(--sp-1) var(--sp-3);
  border-radius: var(--radius-pill);
  background: var(--accent-soft);
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

/* ---- the roster panel.
 *
 * A dense list rather than a table: on a 360px phone a five-column table needs
 * horizontal scroll, and the one thing this panel exists to make scannable is
 * exactly the column that would scroll off. Each row is a three-part grid that
 * collapses to two stacked blocks below 720px.
 */

.roster-panel { display: grid; gap: var(--sp-4); margin-top: var(--gap-stack); }

.roster-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-3);
  flex-wrap: wrap;
}

.roster-head h2 { margin-bottom: var(--sp-1); }
.roster-head .eyebrow { margin-bottom: var(--sp-1); }
.roster-head .muted { margin: 0; font-size: var(--text-sm); }

.roster-move {
  display: grid;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
}

.roster-move-line { margin: 0; }
.roster-move-line .eyebrow { margin: 0; }

.roster-move-row {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  flex-wrap: wrap;
  font-size: var(--text-sm);
}

.roster-groups { display: grid; gap: var(--sp-5); }
.roster-group { display: grid; gap: var(--sp-2); }

.roster-group-title {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0;
  color: var(--ink-subtle);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

/* A short accent rule rather than a heavier type treatment: the groups need to
 * be findable while scrolling without competing with the player names. */
.roster-group-title::before {
  content: "";
  width: 14px;
  height: 2px;
  border-radius: var(--radius-pill);
  background: var(--accent-bright);
}

.roster-row {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: var(--sp-2) var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.roster-row:hover {
  border-color: var(--line-strong);
  box-shadow: var(--shadow-sm);
}

/* ---- how loudly a night is allowed to speak.
 *
 * Three steps, not two. The earlier pass left everything except a big game and a
 * collapse in flat grey, on the principle that ordinary variance must not be
 * dressed up as a crisis. That principle is right and is kept -- what changes is
 * that "quiet" and "above his line" now get a wash at roughly a third of the
 * strength, so the eye can rank a roster at a glance while the loud colours stay
 * reserved for the two verdicts that actually warrant a second look.
 *
 * "Normal for him" stays completely uncoloured on purpose. It is the baseline
 * the other four are read against, and tinting it would leave nothing neutral. */
.roster-row[data-verdict="big_up"] {
  border-left: 3px solid var(--positive);
  background: color-mix(in srgb, var(--positive) 7%, var(--surface));
}

.roster-row[data-verdict="up"] {
  border-left: 3px solid color-mix(in srgb, var(--positive) 45%, transparent);
  background: color-mix(in srgb, var(--positive) 3%, var(--surface));
}

.roster-row[data-verdict="down"] {
  border-left: 3px solid color-mix(in srgb, var(--caution) 45%, transparent);
  background: color-mix(in srgb, var(--caution) 4%, var(--surface));
}

.roster-row[data-verdict="big_down"] {
  border-left: 3px solid var(--negative);
  background: color-mix(in srgb, var(--negative) 7%, var(--surface));
}

/* The drop mark outranks every verdict, including a big game: the answer above
 * is telling you to move this player on, and that has to survive him having had
 * a good night. Last in the cascade so it wins without !important. */
.roster-row[data-role="drop"] {
  border-left: 3px solid var(--negative);
  background: color-mix(in srgb, var(--negative) 9%, var(--surface));
}

/* The left rail: photo with its lineup slot beneath it. Keeping them in one
 * column means the row grid is unchanged by adding faces -- two columns on a
 * phone, three on desktop -- rather than losing another 44px of a 375px screen. */
.roster-figure {
  display: grid;
  justify-items: center;
  gap: var(--sp-1);
}

/* ---- team colour.
 *
 * Data encoding, not brand: these are the thirty clubs' own primaries, used the
 * way a categorical palette is used in a chart, and they are deliberately kept
 * out of the semantic layer in tokens.css so nothing in the chassis can start
 * depending on them. --team is consumed only by the avatar ring and slot chip.
 *
 * Two variables, not one: --team-raw is the club's own primary and --team is
 * what components use. A single variable could not be lightened for dark mode,
 * because a custom property whose value reads itself is a cycle and resolves to
 * nothing -- the ring would simply disappear. */
.roster-row {
  --team-raw: var(--line-strong);
  --team: var(--team-raw);
}

.roster-row[data-team="ATL"] { --team-raw: #e03a3e; }
.roster-row[data-team="BOS"] { --team-raw: #007a33; }
.roster-row[data-team="BKN"] { --team-raw: #3d3d3d; }
.roster-row[data-team="CHA"] { --team-raw: #1d1160; }
.roster-row[data-team="CHI"] { --team-raw: #ce1141; }
.roster-row[data-team="CLE"] { --team-raw: #860038; }
.roster-row[data-team="DAL"] { --team-raw: #00538c; }
.roster-row[data-team="DEN"] { --team-raw: #0e2240; }
.roster-row[data-team="DET"] { --team-raw: #c8102e; }
.roster-row[data-team="GSW"] { --team-raw: #1d428a; }
.roster-row[data-team="HOU"] { --team-raw: #ce1141; }
.roster-row[data-team="IND"] { --team-raw: #002d62; }
.roster-row[data-team="LAC"] { --team-raw: #c8102e; }
.roster-row[data-team="LAL"] { --team-raw: #552583; }
.roster-row[data-team="MEM"] { --team-raw: #5d76a9; }
.roster-row[data-team="MIA"] { --team-raw: #98002e; }
.roster-row[data-team="MIL"] { --team-raw: #00471b; }
.roster-row[data-team="MIN"] { --team-raw: #0c2340; }
.roster-row[data-team="NOP"] { --team-raw: #85714d; }
.roster-row[data-team="NYK"] { --team-raw: #f58426; }
.roster-row[data-team="OKC"] { --team-raw: #007ac1; }
.roster-row[data-team="ORL"] { --team-raw: #0077c0; }
.roster-row[data-team="PHI"] { --team-raw: #006bb6; }
.roster-row[data-team="PHX"] { --team-raw: #5f259f; }
.roster-row[data-team="POR"] { --team-raw: #e03a3e; }
.roster-row[data-team="SAC"] { --team-raw: #5a2d81; }
.roster-row[data-team="SAS"] { --team-raw: #8a8d8f; }
.roster-row[data-team="TOR"] { --team-raw: #ce1141; }
.roster-row[data-team="UTA"] { --team-raw: #002b5c; }
.roster-row[data-team="WAS"] { --team-raw: #002b5c; }

/* Nine of the thirty primaries are near-black navies and greens, which vanish
 * against a dark surface. Lifting every team colour toward white in dark mode
 * fixes all of them in one rule rather than hand-tuning nine exceptions. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .roster-row {
    --team: color-mix(in srgb, var(--team-raw) 55%, #ffffff 45%);
  }
}
:root[data-theme="dark"] .roster-row {
  --team: color-mix(in srgb, var(--team-raw) 55%, #ffffff 45%);
}

/* Initials are the base layer, not a fallback that has to be swapped in: the
 * photo is painted on top of them, so a player without one degrades to a legible
 * monogram instead of a broken-image icon. */
.roster-avatar {
  position: relative;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  overflow: hidden;
  /* Tinted well below the ring so the initials underneath a missing photo keep
   * their contrast against it whatever the club colour is. */
  background: color-mix(in srgb, var(--team) 14%, var(--surface-sunken));
  border: 2px solid var(--team);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--team) 25%, transparent);
}

.roster-initials {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-subtle);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* Headshots are transparent PNGs cropped tight to the head, so they sit high in
 * the frame. Scaling past the circle and pulling down centres the face instead
 * of the bounding box. */
.roster-avatar img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 12%;
  transform: scale(1.28) translateY(6%);
}

/* The slot chip picks up the club colour too, but at a tint rather than the
 * full primary: it carries text, so the fill has to stay far enough from the
 * ink to read at 0.78rem in both themes. */
.roster-slot {
  min-width: 2.5rem;
  padding: var(--sp-1) var(--sp-2);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--team) 16%, var(--surface-sunken));
  color: var(--ink-muted);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 600;
  text-align: center;
}

.roster-id { display: grid; gap: var(--sp-1); min-width: 0; }

.roster-name-line {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

.roster-name-line strong { font-size: var(--text-base); }
.roster-facts { color: var(--ink-subtle); font-size: var(--text-xs); }

.roster-game { grid-column: 2; display: grid; gap: var(--sp-1); min-width: 0; }

.roster-verdict-row {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
}

/* The signed number takes the verdict's own colour. It is the single value the
 * whole row exists to deliver, so it should not be the greyest thing in it. */
.roster-delta {
  font-family: var(--font-mono);
  font-size: var(--text-base);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  letter-spacing: var(--tracking-tight);
}

.roster-row[data-verdict="big_up"] .roster-delta,
.roster-row[data-verdict="up"] .roster-delta { color: var(--positive); }
.roster-row[data-verdict="down"] .roster-delta { color: var(--caution); }
.roster-row[data-verdict="big_down"] .roster-delta { color: var(--negative); }
.roster-row[data-verdict="normal"] .roster-delta { color: var(--ink-subtle); }

.roster-note { color: var(--ink-muted); font-size: var(--text-sm); line-height: var(--leading-snug); }
.roster-context { color: var(--ink-subtle); font-size: var(--text-xs); }
.roster-verdict.muted { font-size: var(--text-sm); font-weight: 600; }

/* The deviation bar. Centre is the player's own season average, so a fill to the
 * right is a game above his line and to the left below it -- the comparison is
 * per player, never against the league. */
.roster-bar {
  position: relative;
  flex: 1 1 80px;
  max-width: 160px;
  height: 6px;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
}

.roster-bar::before {
  content: "";
  position: absolute;
  left: 50%;
  top: -2px;
  bottom: -2px;
  width: 1px;
  background: var(--line-strong);
}

/* The fill fades out toward the centre line so the bar reads as a departure from
 * the average rather than as a quantity measured from the left edge -- the point
 * of a diverging scale is that zero is in the middle. */
.roster-bar i {
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--dev, 0%);
  border-radius: var(--radius-pill);
  background: var(--ink-subtle);
}

.roster-bar[data-direction="up"] i {
  left: 50%;
  background: linear-gradient(
    to right,
    color-mix(in srgb, var(--positive) 45%, transparent),
    var(--positive)
  );
}

.roster-bar[data-direction="down"] i {
  right: 50%;
  background: linear-gradient(
    to left,
    color-mix(in srgb, var(--negative) 45%, transparent),
    var(--negative)
  );
}

/* Only the two loud verdicts get the halo, for the same reason only they get the
 * strong wash: it is what makes them findable when scrolling a full roster. */
.roster-row[data-verdict="big_up"] .roster-bar i {
  box-shadow: 0 0 6px color-mix(in srgb, var(--positive) 55%, transparent);
}

.roster-row[data-verdict="big_down"] .roster-bar i {
  box-shadow: 0 0 6px color-mix(in srgb, var(--negative) 55%, transparent);
}

/* Badge fills for the mid-tier verdicts. The .badge.positive / .badge.negative
 * pair already in the system is the loud end; these sit under them so the four
 * scored verdicts read as a scale instead of two colours and a gap. */
.roster-verdict-row .badge.soft-positive {
  background: color-mix(in srgb, var(--positive) 9%, transparent);
  color: var(--positive);
}

.roster-verdict-row .badge.soft-caution {
  background: color-mix(in srgb, var(--caution) 11%, transparent);
  color: var(--caution);
}

@media (min-width: 720px) {
  .roster-row { grid-template-columns: auto minmax(0, 1fr) minmax(0, 1.4fr); align-items: center; }
  .roster-game { grid-column: 3; }
}

/* ---- the transaction controls.
 *
 * The confirm block is deliberately the loudest thing in the panel. Everything
 * else here reports; this is the only control that changes a real league, and
 * the drop half of a swap is invisible in the headline that offers it. */

.roster-transaction { display: grid; gap: var(--sp-3); }

.roster-block-list,
.roster-confirm-list {
  display: grid;
  gap: var(--sp-2);
  margin: var(--sp-2) 0 0;
  padding-left: var(--sp-5);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

.roster-confirm-list { padding-left: 0; list-style: none; }
.roster-confirm-list li { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }

.roster-confirm {
  display: grid;
  gap: var(--sp-3);
  padding: var(--sp-4);
  border: 1px solid color-mix(in srgb, var(--negative) 40%, transparent);
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--negative) 6%, var(--surface));
  box-shadow: var(--shadow-sm);
}

.roster-confirm > strong { font-size: var(--text-lg); }

/* The categories that moved, in a 9-cat league. Direction rather than magnitude
 * is what a category chip carries: in a category league you win it or you do
 * not, so the tint says which and the number says by how much. */
.roster-cats { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-top: var(--sp-1); }

.roster-cat {
  display: inline-flex;
  align-items: baseline;
  gap: var(--sp-1);
  padding: 2px var(--sp-2);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
}

.roster-cat-label { color: var(--ink-subtle); font-weight: 600; letter-spacing: 0.02em; }
.roster-cat-value { font-family: var(--font-mono); font-weight: 600; }

.roster-cat[data-direction="up"] {
  background: color-mix(in srgb, var(--positive) 12%, transparent);
}
.roster-cat[data-direction="up"] .roster-cat-value { color: var(--positive); }

.roster-cat[data-direction="down"] {
  background: color-mix(in srgb, var(--negative) 12%, transparent);
}
.roster-cat[data-direction="down"] .roster-cat-value { color: var(--negative); }
