/* PLAN PHASE 4: full visual design system. Token-based (CSS variables) so
   dark and light mode are each a complete, separately-reasoned palette, not
   a mechanical inversion of the other -- see the "why" notes on the color
   blocks below. Defaults to dark (a personal tool checked at odd hours --
   early practice reports, a quick look before kickoff -- is dark-dominant),
   toggled via a button in the nav and persisted in localStorage; see the
   toggle script at the bottom of this file's companion JS in layout.html.

   ONE RULE that keeps this from re-growing a rainbow over time: any new
   color must be either one of the four event hues (in one of its defined
   roles), the accent, the watchlist gold, or a --text/--bg gray tier.
   Nothing else. */

:root {
  /* ---- dark (default) ---- */
  --bg: #101418;
  --bg-raised: #1a2026;
  --bg-hover: #1d242b;
  --hairline: rgba(255, 255, 255, 0.07);
  --hairline-strong: rgba(255, 255, 255, 0.14);

  --text: #e6e9ec;
  --text-2: #98a2ad;
  --text-3: #86929d;

  /* Each event hue exists in exactly one role here (text=border=glyph) plus
     a 7%-alpha wash derived from the SAME hue, so all four sit at identical
     perceived intensity by construction, not hand-tuned luck. */
  --c-injury: #f2695f;
  --bg-injury: rgba(242, 105, 95, 0.07);
  --c-practice: #dcb35e;
  --bg-practice: rgba(220, 179, 94, 0.07);
  --c-roster: #7fb0e0;
  --bg-roster: rgba(127, 176, 224, 0.07);
  --c-rumor: #bd9be0;
  --bg-rumor: rgba(189, 155, 224, 0.07);

  --accent: #4d8dd6;
  --flash: rgba(77, 141, 214, 0.18);
  --watchlist: #e0a868;
  --bg-watchlist: rgba(224, 168, 104, 0.12);

  --shadow: none; /* elevation is a lighter surface in dark mode, never a shadow -- shadows don't read on dark */
  --warn-border: #7d6320;
  --warn-bg: rgba(196, 152, 46, 0.08);
  --warn-text: #d0a23f;
}

:root[data-theme="light"] {
  /* ---- light: re-derived, not inverted ---- */
  --bg: #f7f7f5;
  --bg-raised: #ffffff;
  --bg-hover: #f0f0ed;
  --hairline: rgba(16, 20, 24, 0.09);
  --hairline-strong: rgba(16, 20, 24, 0.16);

  --text: #191d22;
  --text-2: #555e68;
  --text-3: #5d666f;

  /* Colored TEXT needs heavier weight to clear contrast on a white surface
     than a colored BORDER does -- dark mode gets away with one value per hue
     because light-on-dark has more contrast headroom to begin with. */
  --c-injury: #b3251c;
  --bg-injury: rgba(220, 74, 64, 0.09);
  --c-practice: #8d620a;
  --bg-practice: rgba(180, 130, 20, 0.09);
  --c-roster: #1e40af;
  --bg-roster: rgba(30, 64, 175, 0.08);
  --c-rumor: #6d28d9;
  --bg-rumor: rgba(109, 40, 217, 0.08);

  --accent: #1a3d7c;
  --flash: rgba(202, 138, 4, 0.16);
  --watchlist: #aa4f09;
  --bg-watchlist: rgba(217, 119, 6, 0.12);

  --shadow: 0 4px 16px rgba(16, 20, 24, 0.1); /* elevation returns to being a real shadow on light */
  --warn-border: #c9a44a;
  --warn-bg: rgba(176, 132, 26, 0.10);
  --warn-text: #8a6512;
}

/* Deliberately no @media (prefers-color-scheme) block: this app defaults to
   dark REGARDLESS of OS setting (the stated usage pattern -- 6am practice
   reports, a check before Sunday kickoff -- is dark-dominant), not just
   "dark until told otherwise by the OS." The toggle is the escape hatch for
   the one bright-Sunday-afternoon case; :root[data-theme="light"] above is
   the only way into light mode. */

/* ---- type: system UI + system mono, deliberately no webfont -- a personal
   tool checked on odd hours/mobile connections shouldn't pay a font-network
   round trip, and SF Pro/SF Mono (or their platform equivalents) are already
   excellent dense-UI faces. The meta row (timestamp/team/category/source)
   goes ENTIRELY monospace: team codes align into a column, digits in
   timestamps line up, and the mono texture itself signals "this line is
   metadata," separating it from the proportional headline below it. ---- */
:root {
  --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

* {
  box-sizing: border-box;
}

body {
  font-family: var(--font-ui);
  max-width: 900px;
  margin: 0 auto;
  padding: 16px;
  color: var(--text);
  background: var(--bg);
}

a {
  color: var(--accent);
}

nav {
  display: flex;
  align-items: center;
  gap: 4px;
}

nav a {
  margin-right: 16px;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}

nav a:hover {
  text-decoration: underline;
}

/* Theme toggle -- defaults to dark (or the OS preference, until the user
   picks explicitly); the button just flips data-theme and remembers it. */
#theme-toggle {
  margin-left: auto;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  color: var(--text-2);
  border-radius: 6px;
  padding: 4px 10px;
  font-size: var(--fs-sm);
  cursor: pointer;
}

/* Breakouts page: the board is a whole separate app's self-contained HTML
   (own CSS/JS, own ~1180px layout), embedded in an iframe under this app's
   own nav so navigating there feels like part of the site rather than a
   jump to a different page -- see app/main.py's /breakouts route. The 900px
   body column everywhere else would squeeze that board pointlessly, so this
   one page goes full-bleed and turns the page into a fixed-height flex
   column: nav on top (unchanged), then the toolbar, then the iframe filling
   whatever's left. */
body.page-full {
  max-width: none;
  height: 100vh;
  padding: 16px 16px 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* Keeps the nav links in the exact same horizontal position as every other
   page: those pages get this centering for free from body's own 900px
   max-width, which page-full's body deliberately gives up so the board
   below can go full-bleed -- so nav has to opt back into it on its own,
   or the links jump to the true viewport edge instead of the edge of the
   900px column everyone's used to seeing them at. */
body.page-full nav {
  flex: none;
  width: 100%;
  max-width: 900px;
  margin: 0 auto;
}

body.page-full main {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  margin: 12px -16px 0;
}

.breakout-toolbar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  padding: 8px 16px;
  background: var(--bg-raised);
  border-top: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
  font-size: var(--fs-base);
  color: var(--text-2);
}

.breakout-toolbar form {
  margin: 0;
}

.breakout-toolbar button[type="submit"] {
  background: var(--bg);
  color: var(--text-2);
  border: 1px solid var(--hairline-strong);
  border-radius: 5px;
  padding: 3px 10px;
  font-size: var(--fs-sm);
  cursor: pointer;
}

.breakout-toolbar .toolbar-spacer {
  margin-left: auto;
}

.breakout-frame {
  flex: 1;
  min-height: 0;
  width: 100%;
  border: none;
  background: var(--bg);
}

.alert-badge {
  background: var(--bg-injury);
  border: 1px solid var(--c-injury);
  color: var(--c-injury);
  border-radius: 5px;
  padding: 3px 10px;
  font-size: var(--fs-sm);
  cursor: pointer;
}

.alert-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  z-index: 49;
}

.alert-sidebar {
  position: fixed;
  top: 0;
  right: -380px;
  width: 360px;
  height: 100vh;
  background: var(--bg-raised);
  border-left: 1px solid var(--hairline-strong);
  z-index: 50;
  transition: right 0.2s ease;
  overflow-y: auto;
  font-size: var(--fs-base);
  color: var(--text);
  padding: 16px 18px;
}

.alert-sidebar-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 4px;
}

.alert-close {
  background: none;
  border: none;
  color: var(--text-2);
  font-size: var(--fs-lg);
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
}

.alert-intro {
  color: var(--text-2);
  margin-bottom: 10px;
}

.alert-list {
  margin: 0;
  padding-left: 20px;
}

.alert-list li {
  margin-bottom: 10px;
}

.alert-meta {
  color: var(--text-2);
}

.alert-more {
  background: none;
  border: none;
  color: var(--accent);
  cursor: pointer;
  padding: 8px 0 0;
  font-size: var(--fs-base);
  text-decoration: underline;
}

.filters {
  display: flex;
  gap: 8px;
  margin: 16px 0;
  flex-wrap: wrap;
}

.filters input {
  padding: 4px 6px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
}

.multiselect {
  position: relative;
}

.multiselect > summary {
  list-style: none;
  cursor: pointer;
  padding: 4px 10px;
  border: 1px solid var(--hairline-strong);
  border-radius: 4px;
  background: var(--bg-raised);
  color: var(--text);
  font-size: var(--fs-md);
  user-select: none;
}

.multiselect > summary::-webkit-details-marker {
  display: none;
}

.multiselect[open] > summary {
  border-color: var(--accent);
}

.multiselect-panel {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  z-index: 10;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  border-radius: 6px;
  box-shadow: var(--shadow);
  padding: 8px;
  min-width: 200px;
  max-height: 320px;
  overflow-y: auto;
}

.multiselect-panel label {
  display: block;
  padding: 3px 4px;
  font-size: var(--fs-md);
  color: var(--text);
  white-space: nowrap;
  border-radius: 3px;
}

.multiselect-panel label:hover {
  background: var(--bg-hover);
}

.multiselect-panel .ms-search {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 6px;
  padding: 4px 6px;
  background: var(--bg);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
}

.multiselect-panel .ms-clear {
  display: block;
  width: 100%;
  margin-bottom: 6px;
  padding: 3px;
  background: var(--bg-hover);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
  cursor: pointer;
  font-size: var(--fs-sm);
}

.new-items-banner {
  margin: 8px 0 16px;
  overflow: hidden;
  max-height: 0;
  opacity: 0;
  transform: translateY(-6px);
  transition: max-height 0.25s ease, opacity 0.25s ease, transform 0.25s ease;
}

.new-items-banner.visible {
  max-height: 60px;
  opacity: 1;
  transform: translateY(0);
}

.new-items-banner button {
  width: 100%;
  padding: 10px;
  font-size: var(--fs-md);
  font-weight: 700;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}

.new-items-banner button:hover {
  filter: brightness(0.9);
}

/* One-line outcome of the "Refresh now" button. Deliberately a quiet hairline
   note, not a card or a toast: it reports on something that happens in the
   background and needs no acknowledging, so it should read and then be ignored.
   Sits on the roster-news blue rather than the accent so it can't be mistaken
   for the "Show new" call-to-action right above it. */
.flash {
  margin: 8px 0 14px;
  padding: 8px 11px;
  font-size: var(--fs-base);
  color: var(--text);
  background: var(--bg-roster);
  border-left: 2px solid var(--c-roster);
  border-radius: 3px;
}

.flash-muted {
  color: var(--text-2);
  background: var(--bg-raised);
  border-left-color: var(--text-3);
}

/* Inline result of "Refresh now", reported next to the button instead of via a
   page navigation. Same quiet register as .flash -- it's a background job's
   receipt, not something to acknowledge. */
.refresh-form {
  display: flex;
  align-items: center;
  gap: 9px;
}

.refresh-status {
  font-size: var(--fs-sm);
  color: var(--text-2);
}

/* Explanatory prose (currently only the Hindsight page's note). Secondary
   text, constrained measure: it's there to be read once. */
.note {
  max-width: 68ch;
  margin: 10px 0 16px;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--text-2);
}

.note strong {
  color: var(--text);
  font-weight: 600;
}

.feed {
  list-style: none;
  padding: 0;
}

/* ---- rows: dense, flush-stacked, hairline-separated -- explicitly NOT
   contemporary card design (16-24px padding, shadows, rounded containers).
   This is a firehose meant for 200-400 scans/day, not a blog meant for one
   article at a time; every pixel of card chrome is ink spent saying "here is
   an item" (already obvious) instead of "what kind of item" (the useful
   fact). Event rows land around 62px; compact/River rows around 46px --
   the density budget the whole redesign was chasing. ---- */
.item {
  border-bottom: 1px solid var(--hairline);
  border-left: 3px solid transparent;
  padding: 7px 12px 7px 10px;
}

.item.priority-injury, .item.priority-injury_report {
  border-left-color: var(--c-injury);
  background: var(--bg-injury);
}
.item.priority-practice_report { border-left-color: var(--c-practice); }
.item.priority-roster_news { border-left-color: var(--c-roster); }
.item.priority-rumor { border-left-color: var(--c-rumor); }

/* An older story is de-emphasised, not hidden. 0.72 dropped its timestamp and
   source to 3.0:1; 0.92 is what keeps the faintest text on a stale row above
   4.5:1 in light mode, which is the tighter of the two themes. The row still
   reads as receded because the dim compounds with text that is already
   secondary. */
.item.item-stale { opacity: 0.92; }

/* Light mode has less contrast headroom before dimmed text drops under
   4.5:1, so it gets a gentler dim than dark mode's 0.72. */
:root[data-theme="light"] .item.item-stale { opacity: 0.92; }

.item-meta {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-3);
  display: flex;
  gap: 8px;
  align-items: baseline;
  margin-bottom: 3px;
  flex-wrap: wrap;
}

.item-meta .time {
  margin-left: auto;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* Event-category glyph + shortcode (INJ/PRAC/MOVE/RUM) -- see the cat_badge
   macro in feed.html. Provenance categories (beat_writer/insider/
   fantasy_analyst/team_site) get NEITHER glyph nor color, just lowercase
   gray text: if everything had a glyph, nothing would, and the glyph's
   entire job is to mean "something happened." */
.cat-badge {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.cat-badge svg {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
}

.cat-injury, .cat-injury_report { color: var(--c-injury); }
.cat-practice_report { color: var(--c-practice); }
.cat-roster_news { color: var(--c-roster); }
.cat-rumor { color: var(--c-rumor); }

.category.cat-provenance {
  text-transform: lowercase;
  font-weight: 400;
  letter-spacing: normal;
  color: var(--text-3);
}

.players {
  font-family: var(--font-ui);
  font-weight: 600;
  font-size: var(--fs-sm);
  color: var(--text);
  background: var(--bg-raised);
  border-radius: 3px;
  padding: 0 6px;
  /* now an <a> that filters the feed to this player */
  text-decoration: none;
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
}

.players:hover { background: var(--hairline-strong); }

/* Position rides inside the player chip. Deliberately quiet by default: on a
   feed where most items name someone, an emphasised position on every chip
   would be noise. Startable positions get the accent, everyone else stays
   grey, so "is this someone I could play?" reads without being read. */
.pos {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: .04em;
  color: var(--text-3);
}

.pos-skill { color: var(--accent); }

.item-title {
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  line-height: 1.35;
  font-weight: 600;
  color: var(--text);
  text-decoration: none;
  display: block;
  margin-top: 1px;
}

.item-title:hover {
  text-decoration: underline;
}

.item-summary {
  margin: 2px 0 0;
  color: var(--text-2);
  font-size: var(--fs-sm);
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* PLAN PHASE 1 -------------------------------------------------------- */

.digest-box {
  margin: 14px 0 20px;
  padding: 10px 14px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
}

.digest-box > summary { cursor: pointer; list-style: none; }
.digest-box > summary::-webkit-details-marker { display: none; }
.digest-box > summary::before {
  content: "\25B8";
  display: inline-block;
  margin-right: 7px;
  color: var(--text-3);
  transition: transform .12s ease;
}
.digest-box[open] > summary::before { transform: rotate(90deg); }
.digest-box[open] > summary { margin-bottom: 7px; }
.digest-count {
  font-family: var(--font-mono);
  font-weight: 400;
  color: var(--text-3);
  letter-spacing: 0;
  text-transform: none;
  margin-left: 8px;
}
@media (prefers-reduced-motion: reduce) {
  .digest-box > summary::before { transition: none; }
}

.digest-head {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  margin-bottom: 6px;
}

.digest-box ul {
  margin: 0;
  padding-left: 18px;
  font-size: var(--fs-base);
  color: var(--text);
}

.digest-box li {
  margin-bottom: 3px;
}

.digest-src {
  color: var(--text-3);
  font-size: var(--fs-sm);
  font-family: var(--font-mono);
}

.band-head {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-2);
  margin: 22px 0 4px;
  padding-top: 10px;
  border-top: 1px solid var(--hairline-strong);
}

.band-count {
  font-weight: 400;
  color: var(--text-3);
  text-transform: none;
  letter-spacing: normal;
}

.feed-compact .item {
  padding: 5px 12px 5px 10px;
}

.feed-compact .item-title {
  font-size: var(--fs-base);
  font-weight: 500;
}

.item-more {
  margin-top: 3px;
}

.item-more summary {
  cursor: pointer;
  font-family: var(--font-ui);
  font-size: var(--fs-sm);
  color: var(--accent);
  list-style: none;
}

.item-more summary::-webkit-details-marker {
  display: none;
}

.item-more summary:before {
  content: "▸ ";
}

.item-more[open] summary:before {
  content: "▾ ";
}

.item-more-list {
  margin: 6px 0 0;
  padding-left: 16px;
  font-size: var(--fs-sm);
  color: var(--text-2);
}

.item-more-list li {
  margin-bottom: 4px;
}

.item-more-list .time {
  color: var(--text-3);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
}

@keyframes item-new-in {
  from { background: var(--flash); }
  to { background: transparent; }
}

.item-new {
  animation: item-new-in 1.8s ease-out;
}

table.sources {
  width: 100%;
  border-collapse: collapse;
  margin-top: 16px;
}

table.sources th, table.sources td {
  border: 1px solid var(--hairline-strong);
  padding: 6px 8px;
  text-align: left;
  font-size: var(--fs-base);
  color: var(--text);
}

tr.error {
  background: var(--bg-injury);
}

/* PLAN PHASE 2 -------------------------------------------------------- */

.view-chips {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
  margin: 4px 0 10px;
}

.chip {
  display: inline-block;
  padding: 4px 12px;
  border: 1px solid var(--hairline-strong);
  border-radius: 20px;
  background: var(--bg-raised);
  color: var(--text);
  font-size: var(--fs-base);
  text-decoration: none;
}

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

.chip.chip-on {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.chip.chip-add {
  border-style: dashed;
  color: var(--text-2);
  cursor: pointer;
  background: none;
}

.chip-group {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}

.chip-del button {
  background: none;
  border: none;
  color: var(--text-3);
  cursor: pointer;
  font-size: var(--fs-md);
  padding: 0 2px;
}

.save-view-form:not([hidden]) {
  display: flex;
  gap: 6px;
  margin: -4px 0 10px;
}

.save-view-form input[type="text"] {
  padding: 4px 6px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
}

.star-btn {
  background: none;
  border: none;
  cursor: pointer;
  font-size: var(--fs-base);
  color: var(--text-3);
  padding: 0;
  line-height: 1;
}

.star-btn.on {
  color: var(--watchlist);
}

/* Watchlist elevation: the strongest treatment on the page -- outranks
   category color, since "my player" outranks "what kind of news." A
   heavier gold border (not just a tint) so it's unmistakable next to the
   four category border colors. */
.item.item-watchlist {
  border-left-width: 5px;
  border-left-color: var(--watchlist);
}

.players.players-watched {
  background: var(--bg-watchlist);
  color: var(--watchlist);
}

/* -- /players page -- */
.ph-title {
  margin: 8px 0 4px;
  color: var(--text);
}

.ph-sub {
  color: var(--text-2);
  font-size: var(--fs-md);
  max-width: 640px;
  margin: 0 0 18px;
}

.ph-add {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  margin-bottom: 24px;
}

.ph-search-wrap {
  position: relative;
}

#ph-search {
  padding: 6px 8px;
  width: 220px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
}

.ph-results {
  position: absolute;
  top: calc(100% + 2px);
  left: 0;
  z-index: 10;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  border-radius: 6px;
  box-shadow: var(--shadow);
  min-width: 220px;
  max-height: 240px;
  overflow-y: auto;
}

.ph-result {
  padding: 6px 10px;
  font-size: var(--fs-md);
  color: var(--text);
  cursor: pointer;
}

.ph-result:hover {
  background: var(--bg-hover);
}

.ph-add select, .ph-add input[type="text"] {
  padding: 5px 6px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
}

.ph-group {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
  margin: 20px 0 6px;
}

.ph-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.ph-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--hairline);
}

.ph-name {
  font-weight: 600;
  color: var(--text);
}

.ph-note {
  color: var(--text-3);
  font-size: var(--fs-base);
}

.ph-empty {
  color: var(--text-3);
  font-style: italic;
}

.ph-remove {
  margin-left: auto;
}

.ph-remove button {
  background: none;
  border: none;
  color: var(--text-3);
  cursor: pointer;
  font-size: var(--fs-lg);
}

.ph-remove button:hover {
  color: var(--c-injury);
}

/* Generic small helpers reused across templates */
.teams {
  color: var(--text-3);
  text-decoration: none;
}

.teams:hover { color: var(--text-2); text-decoration: underline; }

/* AUDIT: the timestamp and the provenance tag were the only text on the page
   failing WCAG AA -- 3.35:1 at 11px, against 7.1:1 for body copy. The timestamp
   is the freshness signal, which is the feed's whole reason to exist, so it was
   the least legible version of the most important thing. --text-3 stays for
   genuinely incidental text; these two step up one level. */
.item-meta .time,
.cat-provenance {
  color: var(--text-2);
}

.source {
  color: var(--text-3);
}

/* -- Sleeper integration (fetchers/sleeper.py) -- */
.sleeper-box {
  margin-bottom: 20px;
  padding: 12px 14px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  border-radius: 6px;
  max-width: 640px;
}

.sleeper-status {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  color: var(--text);
}

.sleeper-status form {
  margin: 0;
}

.sleeper-status button {
  font-size: var(--fs-sm);
  padding: 4px 10px;
  background: var(--bg-hover);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
  cursor: pointer;
}

.sleeper-disconnect:hover {
  border-color: var(--c-injury);
  color: var(--c-injury);
}

.sleeper-connect {
  display: flex;
  gap: 8px;
}

.sleeper-connect input {
  padding: 6px 8px;
  background: var(--bg);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
  flex: 1;
}

.sleeper-connect button {
  padding: 6px 12px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.sleeper-result {
  margin: 10px 0 0;
  font-size: var(--fs-base);
  color: var(--text-2);
}

.sleeper-unmatched {
  margin: 4px 0 0;
  font-size: var(--fs-sm);
  color: var(--text-3);
}

.sleeper-error {
  margin: 10px 0 0;
  font-size: var(--fs-base);
  color: var(--c-injury);
}

.ph-sleeper-tag {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--accent);
  background: var(--bg-hover);
  border-radius: 3px;
  padding: 1px 5px;
}

.ph-league-tag {
  font-size: var(--fs-sm);
  color: var(--watchlist);
}

.ph-league-filter {
  margin: -4px 0 18px;
}

/* A source that fetched fine but returned nothing -- the failure mode that was
   invisible until sources.last_item_count existed (see db.mark_source_fetched). */
.sources .zero-items {
  color: var(--c-injury);
  font-weight: 600;
}

/* Marks a row whose timestamp is the STORY's last activity rather than the
   headline item's own post time (see Cluster.latest_iso). Only appears on
   clusters that actually have follow-ups. */
.time-upd {
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  color: var(--text-3);
  text-transform: uppercase;
}

/* Hindsight page: a top-N pick that actually broke out. Uses the roster-news
   blue rather than a new "success" green -- the palette rule at the top of this
   file allows only the four event hues, the accent and the grays. */
.sources tr.asof-hit td {
  background: var(--bg-roster);
}

.sources tr.asof-hit td:last-child {
  color: var(--c-roster);
  font-weight: 700;
}

/* ---------------------------------------------------------------------------
   PHONE (<=720px)
   Added with the viewport meta tag in layout.html; before both existed there
   was no breakpoint anywhere in this file, so phones rendered the 900px desktop
   layout scaled to 38% -- 5.4px headlines, 5x5px star buttons.

   Two things drive every rule below. Text has to survive at arm's length, so
   the type that was sized for a dense desktop feed gets a floor rather than a
   uniform bump. And a finger is not a cursor: anything tappable clears 44px,
   which is why the star grows a hit area rather than a glyph.
   --------------------------------------------------------------------------- */
@media (max-width: 720px) {
  body {
    padding: 12px 14px 40px;
    /* iOS inflates text in rotated/narrow layouts unless told not to; the sizes
       below are already chosen for small screens. */
    -webkit-text-size-adjust: 100%;
  }

  /* Nav wraps instead of squeezing; the theme toggle keeps its own row end. */
  nav { flex-wrap: wrap; gap: 2px 0; row-gap: 6px; }
  nav a { margin-right: 14px; font-size: var(--fs-md); padding: 4px 0; }

  /* The filter bar was already the tallest thing on a phone; stacking each
     control full-width (the first attempt here) made it seven rows and pushed
     the actual news off the first screen entirely. Instead the controls keep
     flowing inline and wrap, sized for a thumb rather than a cursor -- three
     short rows, and SIGNAL stays visible without scrolling. */
  .filters { gap: 6px; margin: 10px 0; align-items: center; }
  .filters .multiselect > summary,
  .filters button {
    min-height: 38px;
    display: inline-flex;
    align-items: center;
    box-sizing: border-box;
  }
  /* The free-text player box is the one control worth a full row -- it takes
     typing, and a cramped input is the classic mobile-form failure. */
  .filters input { min-height: 38px; flex: 1 1 100%; box-sizing: border-box; }
  .filters a { align-self: center; }

  /* Meta line: the timestamp stops being pushed to the far right (which on a
     narrow row stranded it alone on its own line) and simply follows the rest. */
  .item-meta { gap: 6px; font-size: var(--fs-sm); }
  .item-meta .time { margin-left: 0; }

  /* 13x13 glyph -> a 44px target, without changing how it looks. */
  .star-btn {
    min-width: 44px;
    min-height: 44px;
    margin: -12px 0 -12px -12px;   /* absorb the growth so the row height holds */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: var(--fs-lg);
  }

  /* Reading sizes. The summary stops being a single clipped line -- on a narrow
     column one line is a few words, so truncation removed most of the content;
     two wrapped lines carry the gist and still bound the row. */
  .item-title { font-size: var(--fs-lg); line-height: 1.3; }
  .item-summary {
    font-size: var(--fs-md);
    line-height: 1.45;
    white-space: normal;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .item { padding-right: 2px; }

  /* Long source lists in the cluster summary need to wrap, not overflow. */
  .item-more summary { font-size: var(--fs-sm); line-height: 1.45; }
  .item-more-list li { font-size: var(--fs-sm); }

  .digest-box { padding: 10px 12px; }
  .digest-box ul { font-size: var(--fs-md); padding-left: 16px; }
  .digest-box li { margin-bottom: 6px; }

  /* The sources table is 9 columns of health data; it cannot reflow usefully at
     this width, so it scrolls inside its own container instead of forcing the
     whole page sideways. */
  .table-wrap, .sources-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  table { font-size: var(--fs-sm); }
}

/* Screen-reader-only: keeps the h1 in the accessibility tree and out of the
   visual design, which the nav already handles. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* "updated 4m ago" beside Refresh now -- the page's single freshness answer. */
.feed-freshness {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-2);
  margin-left: 8px;
  white-space: nowrap;
}

/* Active-filter readout. Quieter than the view chips above it -- this reports
   state rather than offering an action, and the only action it does carry
   (remove) lives in the x. */
.active-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 8px 0 4px;
}

.af-label {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-3);
}

.af-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  font-size: var(--fs-sm);
  color: var(--text);
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  border-radius: 3px;
  padding: 2px 7px;
  text-decoration: none;
}

.af-chip:hover { border-color: var(--accent); }

.af-group {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-3);
}

.af-x { color: var(--text-3); font-size: var(--fs-base); line-height: 1; }
.af-chip:hover .af-x { color: var(--accent); }

.af-clear {
  font-size: var(--fs-sm);
  color: var(--text-3);
  margin-left: 2px;
}

@media (max-width: 720px) {
  .active-filters { gap: 5px; }
  .af-chip { font-size: var(--fs-base); padding: 5px 9px; }
}

/* Sort toggle. A segmented pair rather than two loose chips: these are two
   states of one setting, and only one can be on. Sits above the view chips,
   which are filters -- a different kind of control, so it reads as its own. */
.sort-toggle {
  display: inline-flex;
  border: 1px solid var(--hairline-strong);
  border-radius: 4px;
  overflow: hidden;
  margin: 8px 0 0;
}

.sort-opt {
  font-size: var(--fs-sm);
  padding: 4px 12px;
  color: var(--text-2);
  text-decoration: none;
  background: var(--bg-raised);
}

.sort-opt + .sort-opt { border-left: 1px solid var(--hairline-strong); }
.sort-opt:hover { color: var(--text); }

.sort-on {
  background: var(--accent);
  color: #fff;
  font-weight: 600;
}

:root[data-theme="light"] .sort-on { color: #fff; }

@media (max-width: 720px) {
  .sort-opt { padding: 8px 16px; font-size: var(--fs-base); }
}

/* -- /settings -- */
.settings-form { display: flex; flex-direction: column; gap: 18px; margin: 14px 0 26px; max-width: 640px; align-items: flex-start; }
/* Without this the flex column stretches the button to the form's full width,
   which reads as a page-level action rather than "save this form". */
.settings-form button { align-self: flex-start; padding: 6px 18px; }
.settings-form fieldset { align-self: stretch; }
.settings-form fieldset {
  border: 1px solid var(--hairline-strong);
  border-radius: 5px;
  padding: 12px 14px 14px;
  display: flex; flex-direction: column; gap: 9px;
  background: var(--bg-raised);
}
.settings-form legend {
  font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3); padding: 0 5px;
}
.set-why { font-size: var(--fs-sm); color: var(--text-3); margin: 0 0 3px; }
.set-opt { display: flex; gap: 9px; align-items: baseline; font-size: var(--fs-base); color: var(--text); cursor: pointer; }
.set-opt span { display: flex; flex-direction: column; gap: 2px; }
.set-opt em { font-style: normal; font-size: var(--fs-sm); color: var(--text-3); line-height: 1.45; }
.set-inline { margin-left: 10px; }
.set-h3 { margin-top: 22px; }

/* Position-weight bars: the settings' effect, shown rather than described. */
.weight-bars { display: flex; flex-direction: column; gap: 5px; margin: 10px 0 14px; max-width: 420px; }
.wb-row { display: grid; grid-template-columns: 42px 1fr 42px; gap: 9px; align-items: center; }
.wb-pos { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-2); }
.wb-track { background: var(--bg); border: 1px solid var(--hairline-strong); border-radius: 2px; height: 12px; overflow: hidden; }
.wb-fill { display: block; height: 100%; background: var(--accent); }
.wb-val { font-family: var(--font-mono); font-size: var(--fs-xs); color: var(--text-3); text-align: right; font-variant-numeric: tabular-nums; }

/* -- /relevance -- */
.rel-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin: 12px 0 8px; }
.rel-card {
  background: var(--bg-raised); border: 1px solid var(--hairline-strong);
  border-radius: 5px; padding: 12px 14px;
  display: flex; flex-direction: column; gap: 8px;
}
.rel-card h3 { margin: 0; font-size: var(--fs-base); color: var(--text); }
.rel-sub, .rel-foot { margin: 0; font-size: var(--fs-sm); color: var(--text-3); line-height: 1.5; }
.rel-empty { margin: 0; font-size: var(--fs-sm); color: var(--text-3); line-height: 1.55; }
.rel-stat { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.rel-stat b { font-size: var(--fs-xl); color: var(--accent); font-variant-numeric: tabular-nums; }
.rel-stat span { font-size: var(--fs-sm); color: var(--text-3); }
.rel-table { width: 100%; border-collapse: collapse; font-size: var(--fs-sm); }
.rel-table td, .rel-table th { padding: 3px 0; border-bottom: 1px solid var(--hairline); text-align: left; color: var(--text-2); }
.rel-table th { font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--text-3); }
.rel-table td.num { text-align: right; font-family: var(--font-mono); font-variant-numeric: tabular-nums; color: var(--text); }
.rel-table td.dim { color: var(--text-3); }
.rel-table td.ok { color: var(--c-ok, #4FA97F); }
.rel-table td.mid { color: #E0A03C; }
.rel-table td.bad { color: var(--c-injury, #F2695F); }
.rel-wide { margin-top: 8px; }
.rel-h3 { margin-top: 24px; }

/* One-time settings nudge. A bordered banner was too much furniture for a
   line you see once; now it reads as a footnote under the toolbar. */
.prefs-nudge {
  margin: 7px 0 0;
  font-size: var(--fs-sm);
  color: var(--text-3);
}

.prefs-nudge a { color: var(--accent); }

/* "Not top news" — sits beside the star as its opposite. Deliberately quieter
   than the star: starring is a keep, this is a correction, and corrections
   shouldn't advertise themselves on every row. */
.ns-btn {
  background: none;
  border: 0;
  color: var(--text-3);
  cursor: pointer;
  font-size: var(--fs-sm);
  line-height: 1;
  padding: 0 2px;
  opacity: .45;
}

.item:hover .ns-btn { opacity: 1; }
.ns-btn:hover { color: var(--c-injury, #F2695F); }
.ns-btn.on { opacity: 1; color: var(--c-injury, #F2695F); }

/* A flagged row stays readable but visibly steps back, so the effect of the
   click is obvious without the row vanishing. */
.item.item-not-signal { opacity: .55; }
.item.item-not-signal .item-title { font-weight: 500; }

@media (max-width: 720px) {
  /* No hover on touch, so the control has to be permanently visible and
     thumb-sized. */
  .ns-btn { opacity: .8; min-width: 44px; min-height: 44px; margin: -12px 0; font-size: var(--fs-md); }
}

.ns-inline { color: var(--c-injury, #F2695F); font-weight: 600; }

/* ---------------------------------------------------------------------------
   FEED TOOLBAR
   One row at rest. The page used to stack seven bands above the first story --
   349px, 45% of a 768px screen. Everything here earns its place by frequency:
   sort and the two quick views are one tap and stay out; the six filter
   controls are occasional and sit behind a single disclosure carrying a count.
   --------------------------------------------------------------------------- */
.toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  margin: 12px 0 0;
}

.toolbar > form.filters {
  display: contents;   /* the form must not become a layout box in the row */
}

.toolbar-gap { flex: 1 1 auto; }

/* Filters disclosure ------------------------------------------------------ */
.filter-panel { position: relative; }

.filter-panel > summary {
  list-style: none;
  cursor: pointer;
  font-size: var(--fs-sm);
  padding: 3px 10px;
  border: 1px solid var(--hairline-strong);
  border-radius: 999px;
  color: var(--text-2);
  background: var(--bg-raised);
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.filter-panel > summary::-webkit-details-marker { display: none; }
.filter-panel > summary::after { content: "\25BE"; color: var(--text-3); font-size: var(--fs-xs); }
.filter-panel[open] > summary { border-color: var(--accent); color: var(--text); }
.filter-panel > summary:hover { color: var(--text); }

/* Count badge: the "is anything on?" answer, per the active-filter pattern. */
.fp-count {
  background: var(--accent);
  color: #fff;
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  line-height: 1;
  padding: 2px 5px;
  font-weight: 600;
}

.filter-panel-body {
  position: absolute;
  z-index: 40;
  top: calc(100% + 6px);
  left: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 10px;
  min-width: 320px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  border-radius: 6px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
}

.filter-panel-body input[type="text"] { flex: 1 1 100%; }

.fp-link {
  background: none;
  border: 0;
  color: var(--accent);
  font-size: var(--fs-sm);
  cursor: pointer;
  padding: 2px 4px;
  text-decoration: underline;
}

/* Refresh, at the far end ------------------------------------------------- */
.refresh-slot {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  margin: 0;
}

.toolbar .refresh-form button {
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  color: var(--text-2);
  border-radius: 999px;
  cursor: pointer;
  font-size: var(--fs-base);
  line-height: 1;
  padding: 4px 9px;
}

.toolbar .refresh-form button:hover { color: var(--text); border-color: var(--accent); }
/* The glyph carries the meaning visually; the word carries it for a reader. */
.sr-label {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.toolbar .feed-freshness { margin-left: 0; }

@media (max-width: 720px) {
  /* Wraps to two short rows rather than one scrolling one; the refresh slot
     keeps the end of whichever row it lands on.
     Every control here clears 44px -- the same floor the rest of the feed got
     when the viewport meta went in. The first version of this toolbar missed
     it and shipped 28px targets, which is the regression that fix existed to
     prevent. */
  .toolbar { gap: 7px; }
  /* Descendant selectors, not child: form.filters is display:contents, so the
     chips inside it are NOT direct children of .toolbar and a ">" silently
     missed them -- they shipped at 25px in the first pass. */
  .toolbar .sort-opt,
  .toolbar .chip,
  .filter-panel > summary,
  .toolbar .refresh-form button {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
  }
  .toolbar .sort-opt { padding: 0 16px; font-size: var(--fs-md); }
  .toolbar .chip { padding: 0 15px; font-size: var(--fs-md); }
  .filter-panel > summary { padding: 0 15px; font-size: var(--fs-md); }
  .filter-panel-body { min-width: min(88vw, 320px); }
  .toolbar .refresh-form button { padding: 0 15px; font-size: var(--fs-lg); }
  /* Controls inside the opened panel hold the same 44px floor. The panel gets
     taller; that is the correct trade on a surface you open deliberately. */
  .filter-panel-body .multiselect > summary,
  .filter-panel-body .fp-link,
  .filter-panel-body input[type="text"],
  .filter-panel-body button {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
    box-sizing: border-box;
  }
  /* No forced break: letting refresh flow in beside Filters gets the whole
     toolbar into two short rows instead of three. */
  .toolbar-gap { flex: 1 1 auto; }
}

/* Vertical rhythm above the feed. With the toolbar collapsed to one row the
   remaining height was mostly margin, not content -- the digest alone carried
   34px of it. Tightened rather than removed: these are still separate things
   and need to read as separate. */
.digest-box { margin: 10px 0 12px; }
.band-head, .feed-band > h2 { margin-top: 14px; }

/* ---------------------------------------------------------------------------
   "NEW SINCE YOU LOOKED", MARKED PER BAND
   First the +N indicator only tinted rows in place, so clicking it could leave
   Signal visually unchanged while the new items sat somewhere in ninety River
   rows. Then everything new was lifted into one band above both -- which fixed
   the invisibility and broke something else: two thirds of arriving volume is
   River, so that band was mostly chatter with the few items that mattered mixed
   in, i.e. the Signal/River split undone at the moment it was most useful.
   New rows now stay in their own band, marked by a rule at the top of it.
   --------------------------------------------------------------------------- */
.band-new-rule {
  display: flex;
  align-items: baseline;
  gap: 9px;
  margin: 8px 0 6px;
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  letter-spacing: 0.02em;
  color: var(--accent);
}

/* The rule is a marker, not a heading -- it sits inside a band that already has
   one, so it stays quieter than .band-head on purpose. */
.bnr-label { font-weight: 600; }

.bnr-clear {
  font-size: var(--fs-xs);
  font-weight: 400;
  color: var(--text-3);
  margin-left: auto;
}

.bnr-clear:hover { color: var(--accent); }

/* A left rail groups the new rows within the band, so "new" reads as an
   annotation on the band rather than a third section competing with it. */
.feed-new {
  border-left: 2px solid var(--accent);
  padding-left: 8px;
  margin-left: 1px;
  margin-bottom: 10px;
}

.band-nonew {
  font-size: var(--fs-sm);
  color: var(--text-3);
  margin: 14px 0 4px;
}

@media (max-width: 720px) {
  .band-new-rule { flex-wrap: wrap; }
  .bnr-clear { margin-left: 0; min-height: 32px; display: inline-flex; align-items: center; }
}

/* "Load older", one per band. The feed used to end at 100 clusters with
   nothing past it -- a few hours of coverage. A button rather than infinite
   scroll: it keeps the end of the list reachable, and loading more news is a
   decision, not something that should happen because you scrolled. */
.load-more {
  display: block;
  width: 100%;
  margin: 8px 0 4px;
  padding: 9px 12px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  border-radius: 5px;
  color: var(--text-2);
  font-size: var(--fs-sm);
  cursor: pointer;
}

.load-more:hover { color: var(--text); border-color: var(--accent); }
.load-more[data-busy] { color: var(--text-3); cursor: default; }

.load-more-end {
  margin: 8px 0 4px;
  font-size: var(--fs-sm);
  color: var(--text-3);
  text-align: center;
}

@media (max-width: 720px) { .load-more { min-height: 44px; font-size: var(--fs-md); } }

.rel-tier {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  padding: 1px 4px;
  border-radius: 2px;
  background: var(--bg);
  border: 1px solid var(--hairline-strong);
  color: var(--text-3);
}

/* ---- accounts, legal, footer --------------------------------------------

   These pages are read once and acted on once, which makes them the opposite
   of the feed: a single narrow column, generous line height, no density. They
   reuse the feed's tokens (no new colors -- see this file's ONE RULE at the
   top) and add exactly one: a red for the delete-account control, drawn from
   the existing injury hue rather than a fifth color, because "this is
   destructive" and "this is an injury" want the same alarm register. */

.auth-card {
  max-width: 44ch;
  margin: 8px auto 40px;
}

.acct-card { max-width: 60ch; }

.auth-form {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin: 16px 0 8px;
}

.auth-form label {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text-2);
  margin-top: 10px;
}

.auth-form input[type="email"],
.auth-form input[type="password"],
.auth-form input[type="text"] {
  padding: 9px 10px;
  font-size: var(--fs-lg); /* below 16px, iOS Safari zooms the whole page on focus */
  background: var(--bg);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
}

.auth-form input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.auth-form button {
  margin-top: 18px;
  align-self: flex-start;
  min-height: 44px;   /* the tap-target floor the toolbar audit set */
  padding: 0 20px;
  font-size: var(--fs-md);
  font-weight: 600;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn-secondary {
  min-height: 44px;
  padding: 0 16px;
  font-size: var(--fs-md);
  background: var(--bg-raised);
  color: var(--text);
  border: 1px solid var(--hairline-strong);
  border-radius: 4px;
  cursor: pointer;
}

.auth-hint {
  margin: 6px 0 0;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--text-3);
}

.auth-alt,
.auth-legal {
  margin-top: 22px;
  font-size: var(--fs-base);
  color: var(--text-2);
}

.auth-legal { font-size: var(--fs-sm); color: var(--text-3); }

.flash-error {
  background: var(--bg-injury);
  border-left-color: var(--c-injury);
}

.acct-h3 { margin-top: 34px; font-size: var(--fs-md); }

.acct-facts {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: 4px 16px;
  margin: 16px 0 0;
  font-size: var(--fs-base);
}

.acct-facts dt { color: var(--text-3); }
.acct-facts dd { margin: 0; color: var(--text); }

.acct-data,
.acct-sessions {
  margin: 12px 0 0;
  padding-left: 18px;
  font-size: var(--fs-base);
  line-height: 1.7;
  color: var(--text-2);
}

.acct-data strong { color: var(--text); }
.acct-ua { display: block; font-size: var(--fs-sm); color: var(--text-3); font-style: normal; }
.acct-logout { margin-top: 18px; }

.acct-danger-h { color: var(--c-injury); }

.acct-danger { border-top: 1px solid var(--hairline); padding-top: 4px; }
.acct-danger .btn-danger {
  margin-top: 18px;
  align-self: flex-start;
  min-height: 44px;
  padding: 0 18px;
  font-size: var(--fs-md);
  font-weight: 600;
  background: transparent;
  color: var(--c-injury);
  border: 1px solid var(--c-injury);
  border-radius: 4px;
  cursor: pointer;
}
.acct-danger .btn-danger:hover { background: var(--bg-injury); }

/* Legal text is the one place in this app where a long measure and real
   leading beat density -- it is prose meant to be read, not scanned. */
.legal {
  max-width: 68ch;
  margin: 8px auto 48px;
  font-size: var(--fs-md);
  line-height: 1.65;
  color: var(--text-2);
}

.legal h3 {
  margin: 28px 0 8px;
  font-size: var(--fs-md);
  color: var(--text);
}

.legal p { margin: 0 0 12px; }
.legal ul { padding-left: 18px; margin: 0 0 12px; }
.legal li { margin-bottom: 7px; }
.legal strong { color: var(--text); font-weight: 600; }
.legal code {
  font-family: var(--font-mono);
  font-size: var(--fs-sm);
  padding: 1px 4px;
  background: var(--bg-raised);
  border-radius: 3px;
}

.legal-date { font-size: var(--fs-sm); color: var(--text-3); margin-top: -6px; }

.legal-switch {
  display: flex;
  gap: 16px;
  margin: 0 0 20px;
  font-size: var(--fs-base);
}

.legal-switch a { text-decoration: none; font-weight: 600; }
.legal-switch a.is-current { color: var(--text-3); pointer-events: none; }

.site-footer {
  max-width: 68ch;
  margin: 28px auto 14px;
  padding: 10px 12px 0;
  border-top: 1px solid var(--hairline);
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--text-3);
}

/* The one line that tells a signed-out reader their data is browser-local.
   Readable text rather than footer gray: it is information, not boilerplate. */
.foot-cta {
  margin: 0 0 6px;
  font-size: var(--fs-sm);
  color: var(--text-2);
}

/* Links and the affiliation note share one row. Wrapping is allowed, but the
   note is last so a narrow screen drops it to a second line rather than
   breaking the links apart. */
.foot-links {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 14px;
  margin: 0;
}

.foot-links a { text-decoration: none; }
.foot-note { margin: 0; color: var(--text-3); }

@media (max-width: 720px) {
  .auth-card, .acct-card { max-width: none; }
  /* Footer links are the only tappable things down here, so they keep the 44px
     floor -- but the row is laid out so the affiliation note wraps below them
     instead of stretching the tappable row to two lines. */
  .foot-links { gap: 0 4px; }
  .foot-links a { min-height: 44px; display: inline-flex; align-items: center; padding: 0 10px 0 0; font-size: var(--fs-base); }
  .foot-links .foot-note { flex-basis: 100%; margin-top: -6px; }
}

/* ---- push opt-in ---------------------------------------------------------
   Hidden by default and revealed by script only when the browser and the
   server can both actually deliver, so nobody is offered a switch that does
   nothing. */
.push-box {
  margin: 0 0 14px;
  padding: 11px 13px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline);
  border-radius: 5px;
}

.push-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 14px;
}

.push-title { font-size: var(--fs-base); }

.push-why {
  margin: 4px 0 0;
  max-width: 58ch;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--text-2);
}

.push-box .btn-secondary { flex: none; min-height: 36px; font-size: var(--fs-base); }

.push-status {
  margin: 8px 0 0;
  font-size: var(--fs-sm);
  color: var(--accent);
}

@media (max-width: 720px) {
  .push-row { flex-direction: column; align-items: stretch; gap: 10px; }
  .push-box .btn-secondary { min-height: 44px; width: 100%; }
}


/* ---- status trail --------------------------------------------------------
   A player's status is a sequence, and the headline can only show its last
   value. This shows the arc -- Questionable, Out, Will miss time -- so a
   downgrade is visible without opening the story. Earlier steps are muted and
   the current one carries the weight, because what matters most is where it
   stands NOW; the history is context for it, not a competing headline. */
.status-trail {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 5px;
  margin: 5px 0 0;
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  letter-spacing: 0.02em;
}

.st-step {
  padding: 1px 6px;
  border-radius: 2px;
  background: var(--bg);
  border: 1px solid var(--hairline-strong);
  color: var(--text-3);
}

.st-now {
  background: var(--bg-injury);
  border-color: var(--c-injury);
  color: var(--c-injury);
  font-weight: 600;
}

.st-arrow { color: var(--text-3); font-size: var(--fs-xs); }

/* ---- emerging board ------------------------------------------------------ */
.emerge-table {
  width: 100%;
  border-collapse: collapse;
  margin: 14px 0 10px;
  font-size: var(--fs-base);
}

.emerge-table th {
  text-align: left;
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-3);
  border-bottom: 1px solid var(--hairline-strong);
  padding: 0 8px 5px;
}

.emerge-table td { padding: 5px 8px; border-bottom: 1px solid var(--hairline); }
.emerge-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.emerge-table .dim { color: var(--text-3); }
.emerge-table a { text-decoration: none; }

/* Your own players get the same gold treatment the feed uses, so the page reads
   the same way the rest of the app does. */
.emerge-mine { background: var(--bg-watchlist); }
.emerge-mine a { color: var(--watchlist); font-weight: 600; }

.emerge-tag {
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  padding: 1px 4px;
  margin-left: 5px;
  border-radius: 2px;
  background: var(--bg);
  border: 1px solid var(--hairline-strong);
  color: var(--text-3);
}

.emerge-rookie { color: var(--c-roster); border-color: var(--c-roster); }

/* A bar behind the number rather than beside it: the probability is the thing
   being compared down the column, and a sparkline in its own cell would push
   the number out of alignment. */
.emerge-bar {
  display: inline-block;
  width: var(--p);
  max-width: 70px;
  height: 3px;
  margin-right: 6px;
  vertical-align: middle;
  background: var(--accent);
  border-radius: 2px;
}

@media (max-width: 720px) {
  .emerge-table { font-size: var(--fs-sm); }
  .emerge-table th:nth-child(5), .emerge-table td:nth-child(5) { display: none; }
  .emerge-bar { display: none; }
}

/* The empty states live in the same <ul> as real player rows, and that list is
   a flexbox -- so a sentence containing <b> was laid out as flex ITEMS and
   broke into columns mid-sentence. They are prose, not rows. */
.ph-list li.ph-empty {
  display: block;
  font-style: normal;
  color: var(--text-3);
}

/* Connecting a league. The list of what you have connected is the common case
   and stays visible; the form that adds one folds away behind a summary. */
.conn-add {
  margin-top: 10px;
  border-top: 1px solid var(--hairline);
  padding-top: 10px;
}

.conn-add > summary {
  cursor: pointer;
  color: var(--text-dim);
  font-size: var(--fs-base);
}

.conn-add > summary:hover { color: var(--text); }

.conn-add .sleeper-connect,
.conn-add > .ph-note,
.conn-line { margin-top: 10px; }

.conn-line {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.conn-line a.btn-secondary { text-decoration: none; }

.conn-narrow { flex: 0 0 120px !important; }

.espn-connect { margin-top: 14px; }

/* The private-ESPN warning. Amber rather than the error red used for failures:
   this is a decision to weigh, not something that has gone wrong. */
.espn-private {
  margin-top: 8px;
  padding: 10px 12px;
  border: 1px solid var(--warn-border);
  border-radius: 5px;
  background: var(--warn-bg);
}

.espn-private > summary {
  cursor: pointer;
  color: var(--warn-text);
  font-size: var(--fs-base);
  font-weight: 600;
}

.espn-warn {
  margin: 8px 0;
  font-size: var(--fs-base);
  line-height: 1.5;
  color: var(--text);
}

.espn-private textarea {
  width: 100%;
  margin-top: 6px;
  padding: 6px 8px;
  background: var(--bg);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
  font-family: inherit;
  font-size: var(--fs-sm);
  box-sizing: border-box;
}

.espn-private textarea:disabled { opacity: 0.5; }

/* A list of connections, not one connection. With three platforms the rows
   used to run together -- a wrapped Disconnect button read as a row of its
   own -- so each gets its own band and the platform names align into a column
   you can scan down. */
.sleeper-box .sleeper-status {
  padding: 9px 0;
  justify-content: flex-start;
  /* nowrap is what makes the ellipsis below work at all: a wrapping flex
     container moves items to a new line rather than shrinking them, so the
     buttons jumped to their own row instead of the name giving way. */
  flex-wrap: nowrap;
}


.sleeper-box .sleeper-status + .sleeper-status {
  border-top: 1px solid var(--hairline);
}

.sleeper-box .sleeper-status:first-child { padding-top: 0; }

.conn-on {
  flex: 0 0 54px;
  font-size: var(--fs-sm);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* Priority when a row is too tight: the league name gives way first. The
   buttons and the "synced 5h ago" stamp keep their shape, because a button
   whose label has broken across two lines looks broken. */
.sleeper-status .ph-note {
  flex: 1 1 auto;
  white-space: nowrap;
}

.sleeper-status button { white-space: nowrap; }

.conn-foot { margin-top: 8px; }

/* League names vary from "Work" to "The League That Shall Not Be Named", and a
   long one used to push the buttons onto their own line. The name gives way
   instead: it truncates, with the full text on hover. */
.sleeper-status b {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 560px) {
  /* A phone has no room for a one-line row, so let it wrap and show the whole
     league name. Placed after the desktop rules deliberately: same specificity,
     so source order is what decides. */
  .sleeper-box .sleeper-status { flex-wrap: wrap; }

  .sleeper-status b {
    overflow: visible;
    white-space: normal;
    flex: 1 1 100%;
  }

  /* Three fields in a row runs off the side of a phone. Same for the
     add-a-player form, which has been pushing its note field and Add button
     past the right edge of the screen. */
  .ph-add { flex-wrap: wrap; }
  .ph-search-wrap { flex: 1 1 100%; }
  .ph-add #ph-search { width: 100%; box-sizing: border-box; }
  .ph-add input[name="note"] { flex: 1 1 120px; min-width: 0; }

  .sleeper-connect { flex-wrap: wrap; }
  .sleeper-connect input { flex: 1 1 100%; }
  .conn-narrow { flex: 1 1 100% !important; }
  .sleeper-connect button { flex: 1 1 auto; }
}

/* Platform chooser: pick one, set up one. */
.conn-pick {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 12px;
}

.conn-chip {
  padding: 6px 14px;
  font-size: var(--fs-base);
  font-family: inherit;
  background: var(--bg);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 999px;
  cursor: pointer;
  text-decoration: none;
  line-height: 1.5;
}

.conn-chip:hover { background: var(--bg-hover); }

.conn-chip.is-on {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

/* Yahoo while access is pending: present, obviously not available, and it says
   why on hover instead of leaving people guessing whether it is broken. */
.conn-chip.is-waiting {
  opacity: 0.45;
  cursor: not-allowed;
}

.conn-chip.is-waiting:hover { background: var(--bg); }

.conn-chip-wrap {
  position: relative;
  display: inline-flex;
  /* The wrapper itself has to outrank the form below it, or the tooltip paints
     underneath the very first input. */
  z-index: 20;
}

.conn-chip-wrap::after {
  content: attr(data-tip);
  position: absolute;
  /* Below the chip, not above: above put it straight through the "Connect a
     league" summary line. */
  top: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  padding: 5px 9px;
  /* Inverted: text colour as the ground. Every surface near this chip -- the
     page, the raised box, the input right below -- is one of two near-identical
     darks, so a tooltip drawn in either had no visible edge and read as loose
     text lying across the form. Inverting is the only choice here that cannot
     collide, in light mode as well as dark. */
  background: var(--text);
  border-radius: 5px;
  color: var(--bg);
  font-size: var(--fs-sm);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
}

.conn-chip-wrap:hover::after,
.conn-chip-wrap:focus-within::after { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .conn-chip-wrap::after { transition: none; }
}

.conn-panel { margin-top: 12px; }

.conn-mark {
  display: inline-block;
  vertical-align: -3px;
  margin-right: 6px;
  flex: 0 0 auto;
}

.conn-mark rect { fill: var(--mark); }
.conn-mark text { fill: #fff; }

.conn-chip.is-on .conn-mark rect { fill: #fff; }
.conn-chip.is-on .conn-mark text { fill: var(--mark); }

.conn-chip { display: inline-flex; align-items: center; }

.conn-on { display: inline-flex; align-items: center; }

/* The Sleeper mark is their real artwork, so it carries its own rounded tile
   and must not be recoloured the way the drawn squares are. */
img.conn-mark { border-radius: 4px; }

/* The one thing the feed has to say to someone who has never been here. Sits
   above the first band, vanishes the moment a league is connected. Sized like a
   toolbar row rather than a hero: the feed is the product and stays on screen. */
.connect-strip {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin: 0 0 16px;
  padding: 12px 14px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
}

.cs-copy { flex: 1 1 260px; min-width: 0; }

.cs-copy strong {
  display: block;
  font-size: var(--fs-md);
  color: var(--text);
}

.cs-copy span {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

.cs-act {
  display: flex;
  gap: 8px;
  flex: 0 1 auto;
}

.cs-act input {
  padding: 6px 9px;
  min-width: 170px;
  background: var(--bg);
  border: 1px solid var(--hairline-strong);
  color: var(--text);
  border-radius: 4px;
  font-family: inherit;
}

.cs-act button {
  padding: 6px 16px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 4px;
  font-family: inherit;
  font-size: var(--fs-base);
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.cs-act button:hover { filter: brightness(1.08); }

.cs-alt {
  font-size: var(--fs-sm);
  color: var(--text-dim);
  white-space: nowrap;
}

@media (max-width: 560px) {
  .cs-act { flex: 1 1 100%; }
  /* A text input carries an intrinsic width from its default `size`, and
     flex-basis auto lets it keep that, so on a narrow screen the button was
     pushed off the edge. Zero basis makes it take what is left instead. */
  .cs-act input {
    min-width: 0;
    width: 0;
    flex: 1 1 0;
  }
}

/* Asked where it is earned: right under a finished sync, above the list of
   players it is offering to protect. */
.keep-cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 20px;
  padding: 12px 14px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
}

.keep-cta strong {
  display: block;
  font-size: var(--fs-md);
  color: var(--text);
}

.keep-cta span {
  display: block;
  margin-top: 2px;
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

.keep-btn {
  padding: 7px 16px;
  background: var(--accent);
  color: #fff;
  border-radius: 4px;
  font-size: var(--fs-base);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}

.keep-btn:hover { filter: brightness(1.08); }

/* Tabs for the three research views, now that they share one nav entry. Sits
   directly under the header on those pages and nowhere else. */
.sub-nav {
  display: flex;
  gap: 4px;
  margin-bottom: 14px;
  border-bottom: 1px solid var(--hairline);
}

.sub-tab {
  padding: 7px 14px;
  font-size: var(--fs-base);
  color: var(--text-dim);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
}

.sub-tab:hover { color: var(--text); }

.sub-tab.sub-on {
  color: var(--text);
  font-weight: 600;
  border-bottom-color: var(--accent);
}

/* The board pages run full-bleed, so the tabs need the same gutter the rest of
   their content has rather than sitting flush against the window edge. */
.page-full .sub-nav { padding: 0 14px; }

/* ---- Feel ------------------------------------------------------------------
   One motion vocabulary, so interactions stop being invented per component.
   The site had six transitions in total before this; nothing acknowledged a
   click, nothing eased open, and a dense tool with no feedback reads as a
   spreadsheet rather than an instrument. */
:root {
  --ease: cubic-bezier(0.2, 0.8, 0.2, 1);
  --fast: 120ms;
  --base: 200ms;
}

/* ---- Type scale ------------------------------------------------------------
   Fifteen distinct sizes had accumulated between 9.5px and 26px -- 9.5, 10,
   10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15, 16, 18, 26. No single one
   was wrong; collectively they were why the page read as assembled rather than
   designed, since half-pixel steps are invisible alone and incoherent together.

   Six steps now. The mapping was chosen so nothing moves more than 1.5px and
   most moves half a pixel or less: this feed's density is what its audience
   comes for, and a type system is not worth spending it on. The one deliberate
   increase is at the bottom -- 9.5px and 10px text is uncomfortable to scan for
   anyone, and it all lands on 11px. */
:root {
  --fs-xs: 11px;
  --fs-sm: 12.5px;
  --fs-base: 13.5px;
  --fs-md: 15px;
  --fs-lg: 18px;
  --fs-xl: 26px;
}

/* Row hover. The single cheapest change to how the feed FEELS: in a list this
   dense, the eye needs confirmation of where it is. */
.feed .item {
  transition: background var(--fast) var(--ease),
              border-left-color var(--fast) var(--ease);
}

.feed .item:hover { background: var(--bg-hover); }

/* Injury rows already carry a tint, so hovering must lift them off their own
   background rather than replace it with a lighter neutral. */
.feed .item.priority-injury:hover,
.feed .item.priority-injury_report:hover {
  background: color-mix(in srgb, var(--bg-hover) 70%, var(--c-injury) 12%);
}

/* The star sat at full strength on every row, so eight rows meant a column of
   eight identical glyphs competing with the headlines. It recedes until the
   row is hovered, or until it is actually on. */
.star-btn {
  /* 0.35 measured 1.4:1 against the row -- below the 3:1 a control needs just
     to be findable, so the star was effectively invisible until hovered.
     Receding is right; disappearing is not. 0.75 is the lowest that clears 3:1
     in BOTH themes, light being the tighter of the two. */
  opacity: 0.75;
  transition: opacity var(--fast) var(--ease),
              transform var(--fast) var(--ease),
              color var(--fast) var(--ease);
}

.item:hover .star-btn { opacity: 0.9; }
.star-btn:hover { opacity: 1; transform: scale(1.25); }
.star-btn.on { opacity: 1; }
.star-btn:active { transform: scale(0.9); }

/* Starring is a decision worth acknowledging. */
.star-btn.just-starred { animation: star-pop 320ms var(--ease); }

@keyframes star-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.45); }
  100% { transform: scale(1); }
}

/* ---- Severity ----
   Every row used to weigh the same: a season-ending surgery and an offensive
   lineman's waiver claim, separated only by a small badge. The left rail
   already encodes CATEGORY by colour; it now encodes STAKES by weight, so the
   rows that end someone's week are visible without reading a word. */
.item.sev-extended { border-left-width: 6px; }
.item.sev-out      { border-left-width: 5px; }
.item.sev-doubtful { border-left-width: 4px; }

/* A rostered player still outranks everything -- "mine" beats "severe" -- so
   the watchlist rail keeps its own colour and takes the heavier of the two. */
.item.item-watchlist.sev-extended,
.item.item-watchlist.sev-out { border-left-width: 6px; }

/* ---- Story expansion ----
   Thirteen rows used to appear instantly and shove the page down, which loses
   the reader's place at the exact moment they went looking for detail. */
.item-more-list {
  animation: more-open var(--base) var(--ease);
}

@keyframes more-open {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* Inside the expansion the newest update is the answer to the question that
   made someone open it. That was done by dimming everything else to 72%, which
   measured at 2.2:1 against the surface for the timestamp and source name --
   illegible, and on the densest text in the app. The newest row is marked
   instead, so hierarchy costs nobody their eyesight. */
.item-more-list li:first-child { position: relative; }

.item-more-list li:first-child::before {
  content: "";
  position: absolute;
  left: -9px;
  top: 0.55em;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--accent);
}

.item-more-list li:first-child a { font-weight: 600; }

.item-more summary { transition: color var(--fast) var(--ease); }

.im-srcs {
  margin-left: 6px;
  color: var(--text-3);
  font-weight: 400;
}

@media (prefers-reduced-motion: reduce) {
  .feed .item,
  .star-btn,
  .item-more-list,
  .item-more-list li,
  .item-more summary { transition: none; animation: none; }
  .star-btn:hover, .star-btn:active { transform: none; }
}

/* "+3" against an update: three other outlets ran the same wording. A count
   rather than three more lines, which is what corroboration is actually worth. */
.im-same {
  margin-left: 6px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--bg-hover);
  /* text-2, not text-3: this sits on the raised surface where the faint token
     measured 2.8:1. */
  color: var(--text-2);
  font-size: var(--fs-xs);
  font-family: var(--font-mono);
}

/* The ranking bars are the one place this app shows its own work: change a
   setting, see exactly how far each position moves. They were correct but
   inert. Now they grow into place on load, so saving a setting produces a
   visible answer instead of a page that looks unchanged.

   scaleX rather than width: the value stays server-rendered as an inline
   width, so there is still one source of truth for the number. */
.wb-fill {
  transform-origin: left center;
  animation: wb-grow 520ms var(--ease) both;
}

@keyframes wb-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* Staggered, so the set reads as a comparison being drawn rather than five
   bars twitching at once. */
.wb-row:nth-child(1) .wb-fill { animation-delay: 0ms; }
.wb-row:nth-child(2) .wb-fill { animation-delay: 60ms; }
.wb-row:nth-child(3) .wb-fill { animation-delay: 120ms; }
.wb-row:nth-child(4) .wb-fill { animation-delay: 180ms; }
.wb-row:nth-child(5) .wb-fill { animation-delay: 240ms; }
.wb-row:nth-child(6) .wb-fill { animation-delay: 300ms; }

@media (prefers-reduced-motion: reduce) {
  .wb-fill { animation: none; }
}

/* Emerging: position filter + sortable columns. */
.em-controls {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 14px 0 10px;
}

.em-pos { display: flex; gap: 6px; }

.em-chip {
  padding: 4px 12px;
  font-family: inherit;
  font-size: var(--fs-sm);
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  color: var(--text-2);
  border-radius: 999px;
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.em-chip:hover { background: var(--bg-hover); color: var(--text); }

.em-chip.em-on {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.em-count {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.em-sort {
  cursor: pointer;
  user-select: none;
  transition: color var(--fast) var(--ease);
}

.em-sort:hover { color: var(--text); }

/* The caret points the way the values run, and only the active column has one,
   so "what is this ordered by" is answered without clicking anything. */
.em-sort::after {
  content: "";
  display: inline-block;
  margin-left: 5px;
  opacity: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  transition: opacity var(--fast) var(--ease), transform var(--fast) var(--ease);
}

.em-sort.em-sorted { color: var(--text); }
.em-sort.em-sorted::after { opacity: 0.8; }
.em-sort.em-sorted.em-asc::after { transform: rotate(180deg); }

.em-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.emerge-table tbody tr { transition: background var(--fast) var(--ease); }
.emerge-table tbody tr:hover { background: var(--bg-hover); }

@media (prefers-reduced-motion: reduce) {
  .em-chip, .em-sort, .em-sort::after, .emerge-table tbody tr { transition: none; }
}

/* Separates the live board from the roll-back years beside it. */
.bt-asof {
  margin: 0 2px 0 8px;
  color: var(--text-3);
  font-size: var(--fs-xs);
}

/* The measured case for the Emerging board, stated where the reader decides
   whether to trust it. */
.em-record {
  margin: 14px 0 16px;
  padding: 12px 14px;
  background: var(--bg-raised);
  border: 1px solid var(--hairline-strong);
  border-left: 3px solid var(--accent);
  border-radius: 6px;
  max-width: 760px;
}

.emr-head {
  font-size: var(--fs-md);
  color: var(--text);
}

.emr-head strong { font-variant-numeric: tabular-nums; }

.emr-vs {
  color: var(--text-2);
  font-size: var(--fs-sm);
}

.emr-sub {
  margin: 6px 0 0;
  font-size: var(--fs-sm);
  color: var(--text-2);
  line-height: 1.5;
}

/* The top N carry the claim; everything under the cut is context. */
.emerge-table tr.em-top td { background: rgba(77, 141, 214, 0.045); }

:root[data-theme="light"] .emerge-table tr.em-top td { background: rgba(26, 61, 124, 0.04); }

.emerge-table tr.em-cut td {
  padding: 5px 0 5px 2px;
  font-size: var(--fs-xs);
  font-family: var(--font-mono);
  color: var(--text-3);
  border-bottom: 1px solid var(--hairline-strong);
  background: none;
}

/* "vs preseason": where this board and the Breakouts board disagree, which is
   the only thing here the other page cannot already tell you. */
.em-gap { font-variant-numeric: tabular-nums; white-space: nowrap; }

.em-riser {
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--bg-watchlist);
  color: var(--watchlist);
  font-size: var(--fs-xs);
  font-weight: 600;
}

.em-unlisted {
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--bg-rumor);
  color: var(--c-rumor);
  font-size: var(--fs-xs);
}

.em-same { color: var(--text-3); font-size: var(--fs-xs); }

/* How many seasons a past board's calls have had to come good. Without it a
   one-year-old board's hit rate reads as comparable to a five-year-old one. */
.bt-hindsight { color: var(--text-3); }

/* ---------------------------------------------------------------------------
   GAME PLAYS
   Live play-by-play, banded away from The River because on a Sunday it is the
   highest-volume thing in the feed and it was burying the analysis and beat
   reporting River exists for. A <details> rather than a plain band: it is for
   glancing at, not reading, so it must not push the rest of the page down.
   --------------------------------------------------------------------------- */
.band-plays > summary.band-head {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: baseline;
  gap: 8px;
}
.band-plays > summary.band-head::-webkit-details-marker { display: none; }
/* The disclosure arrow, drawn rather than inherited so it sits on the baseline
   with the uppercase label and turns with the open state. */
.band-plays > summary.band-head::before {
  content: "\25B8";
  display: inline-block;
  transition: transform 120ms ease;
  color: var(--text-3, var(--text-2));
}
.band-plays[open] > summary.band-head::before { transform: rotate(90deg); }
.band-plays > summary.band-head:hover { color: var(--text); }
.band-plays > summary.band-head:focus-visible {
  outline: 2px solid var(--accent, var(--text-2));
  outline-offset: 2px;
}
.band-note {
  font-family: var(--font-ui);
  font-size: var(--fs-xs, var(--fs-sm));
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-3, var(--text-2));
}
@media (max-width: 560px) {
  /* The explainer is the first thing to go: the label and count carry it. */
  .band-note { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .band-plays > summary.band-head::before { transition: none; }
}

/* ===========================================================================
   RESEARCH (Emerging + Breakouts)
   Rebuilt as one section. They were two pages sharing only a nav strip:
   Emerging a prose document in a 900px column, Breakouts a full-bleed tool
   whose nav sat centred at 900px while its board ran to the viewport edge --
   which is where the "off centre" came from. Between the app nav, the sub-nav,
   the year picker, the board's own topbar and its intro there were seven
   stacked bars before the first ranked row.

   One column for both, one type scale, and the distinction between the two
   pages moved into the tabs themselves.
   =========================================================================== */
body.page-research {
  max-width: var(--rs-col);
}
:root { --rs-col: 1180px; }

/* --- the tabs carry the purpose ------------------------------------------ */
/* Each names its MOMENT above its name and its QUESTION below, so the reader
   never has to infer the difference from a paragraph -- and the lens they are
   not looking at explains itself too, which is what makes the pair legible. */
.rs-tabs {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin: 14px 0 22px;
  max-width: 620px;
}
.rs-tab {
  display: block;
  padding: 9px 13px 10px;
  border: 1px solid var(--hairline);
  border-radius: 10px;
  background: transparent;
  text-decoration: none;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.rs-tab:hover { background: var(--bg-hover); border-color: var(--hairline-strong); }
.rs-tab .rs-when {
  display: block;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}
.rs-tab .rs-name {
  display: block;
  font-size: var(--fs-lg);
  font-weight: 650;
  color: var(--text-2);
  line-height: 1.25;
  margin-top: 1px;
}
.rs-tab .rs-ask {
  display: block;
  font-size: var(--fs-sm);
  color: var(--text-3);
  margin-top: 2px;
}
.rs-tab.rs-on {
  background: var(--bg-raised);
  border-color: var(--accent);
}
.rs-tab.rs-on .rs-name { color: var(--text); }
.rs-tab.rs-on .rs-when { color: var(--accent); }
.rs-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- one sentence, then the numbers -------------------------------------- */
.rs-head { margin: 0 0 16px; }
.rs-title {
  font-size: var(--fs-xl, 22px);
  font-weight: 650;
  letter-spacing: -0.01em;
  margin: 0 0 5px;
  color: var(--text);
  text-wrap: balance;
}
.rs-lede {
  margin: 0;
  max-width: 68ch;
  color: var(--text-2);
  font-size: var(--fs-md);
  line-height: 1.5;
}

/* Three figures are the entire argument for reading the list underneath, and
   they were inside a paragraph that opened with a confidence interval. */
.rs-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 16px 0 14px;
  padding: 0;
}
.rs-stat {
  flex: 0 1 auto;
  min-width: 132px;
  padding: 10px 14px 11px;
  border: 1px solid var(--hairline);
  border-radius: 10px;
  background: var(--bg-raised);
}
.rs-stat dd {
  margin: 0;
  font-size: 26px;
  font-weight: 650;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.rs-stat dt {
  order: 2;
  margin-top: 3px;
  font-size: var(--fs-sm);
  color: var(--text-3);
  line-height: 1.3;
}
.rs-stat { display: flex; flex-direction: column; }
.rs-stat .rs-unit { font-size: 15px; font-weight: 550; margin-left: 1px; color: var(--text-2); }
.rs-stat-key { border-color: color-mix(in srgb, var(--accent) 45%, var(--hairline)); }
.rs-stat-key dd { color: var(--accent); }

/* --- status line --------------------------------------------------------- */
.rs-flag {
  margin: 0 0 14px;
  padding: 8px 12px;
  border-left: 2px solid var(--hairline-strong);
  border-radius: 0 6px 6px 0;
  background: var(--bg-raised);
  color: var(--text-2);
  font-size: var(--fs-sm);
  line-height: 1.45;
}
.rs-flag strong { color: var(--text); }
.rs-flag-muted { background: transparent; color: var(--text-3); }
/* Looking current when it is not is the one failure this page must not have,
   so the stale-season case stays loud while everything else went quiet. */
.rs-flag-warn {
  border-left-color: var(--warn-text);
  background: var(--warn-bg);
  color: var(--warn-text);
}
.rs-flag-warn strong { color: var(--warn-text); }

/* --- controls ------------------------------------------------------------ */
.rs-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0 0 12px;
}
.rs-bar-label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-3);
}
.rs-seg {
  display: inline-flex;
  gap: 4px;
  padding: 3px;
  border: 1px solid var(--hairline);
  border-radius: 9px;
  background: var(--bg-raised);
  flex-wrap: wrap;
}
.rs-chip {
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--text-2);
  font: inherit;
  font-size: var(--fs-sm);
  padding: 4px 11px;
  border-radius: 6px;
  cursor: pointer;
  text-decoration: none;
  line-height: 1.6;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.rs-chip:hover { background: var(--bg-hover); color: var(--text); }
.rs-chip-on { background: var(--accent); color: #fff; font-weight: 550; }
.rs-chip-on:hover { background: var(--accent); color: #fff; }
.rs-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.rs-count { font-size: var(--fs-sm); color: var(--text-3); font-variant-numeric: tabular-nums; }

/* The board's freshness and its mention count: metadata, not a third bar. */
.rs-meta {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0 0 12px;
  font-size: var(--fs-sm);
  color: var(--text-3);
}
.rs-meta .board-age { font-variant-numeric: tabular-nums; }
.rs-meta form { margin: 0; }
.rs-meta .alert-badge {
  appearance: none;
  border: 1px solid var(--hairline);
  background: var(--bg-raised);
  color: var(--text-2);
  font: inherit;
  font-size: var(--fs-sm);
  padding: 3px 10px;
  border-radius: 999px;
  cursor: pointer;
}
.rs-meta .alert-badge:hover { background: var(--bg-hover); color: var(--text); border-color: var(--hairline-strong); }

/* --- table --------------------------------------------------------------- */
.rs-tablewrap { overflow-x: auto; margin: 0 0 18px; }
.rs-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-md);
}
.rs-table th {
  text-align: left;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: 0 10px 7px;
  border-bottom: 1px solid var(--hairline-strong);
  white-space: nowrap;
}
.rs-table td { padding: 7px 10px; border-bottom: 1px solid var(--hairline); }
.rs-table .num { text-align: right; font-variant-numeric: tabular-nums; }
.rs-table th.num { text-align: right; }
.rs-table .rs-n { width: 1%; color: var(--text-3); font-variant-numeric: tabular-nums; text-align: right; }
.rs-table .rs-pos { color: var(--text-3); width: 1%; }
.rs-table a { text-decoration: none; color: var(--text); }
.rs-table a:hover { text-decoration: underline; }
.rs-table tbody tr:hover { background: var(--bg-hover); }
/* The shortlist. Shading stops where the measured edge stops, and both come
   from ACTIONABLE_TOP_N so they cannot drift from the copy. */
.rs-table tr.rs-top td { background: color-mix(in srgb, var(--accent) 5%, transparent); }
.rs-table tr.rs-top:hover td { background: color-mix(in srgb, var(--accent) 9%, transparent); }
.rs-table tr.rs-mine .rs-player a { color: var(--watchlist); font-weight: 600; }
.rs-table tr.rs-cut td {
  padding: 4px 10px;
  border-bottom: 1px dashed var(--hairline-strong);
  font-size: var(--fs-xs, 11px);
  color: var(--text-3);
  text-align: center;
  background: transparent;
}
.rs-tag {
  display: inline-block;
  margin-left: 5px;
  padding: 0 5px;
  border: 1px solid var(--hairline-strong);
  border-radius: 4px;
  font-size: 10px;
  color: var(--text-3);
  vertical-align: 1px;
}
.rs-tag-mine { border-color: var(--watchlist); color: var(--watchlist); }

/* Odds read as a bar first and a number second: the ranking is the point, the
   exact percentage is capped anyway above 45%. */
.rs-odds { white-space: nowrap; }
.rs-meter {
  display: inline-block;
  width: 54px;
  height: 4px;
  margin-right: 8px;
  border-radius: 2px;
  background: var(--hairline-strong);
  vertical-align: middle;
  position: relative;
  overflow: hidden;
}
.rs-meter::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--p, 0%);
  background: var(--accent);
}
.rs-odds-v { font-variant-numeric: tabular-nums; }
.rs-riser { color: var(--watchlist); font-weight: 600; }
.rs-same { color: var(--text-3); }
.rs-unlisted {
  font-size: 10px;
  padding: 1px 5px;
  border: 1px solid var(--hairline-strong);
  border-radius: 4px;
  color: var(--text-3);
}
.rs-sort { cursor: pointer; user-select: none; }
.rs-sort:hover { color: var(--text); }
.rs-sort::after {
  content: "\25BC";
  font-size: 7px;
  margin-left: 4px;
  opacity: 0;
  display: inline-block;
}
.rs-sort.rs-sorted { color: var(--text); }
.rs-sort.rs-sorted::after { opacity: 0.75; }
.rs-sort.rs-sorted.rs-asc::after { transform: rotate(180deg); }
.rs-sort:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- the embedded board -------------------------------------------------- */
/* A tall window rather than a viewport-locked flex column: the old page was
   100vh with overflow hidden, which is why the board's own pinned formula bar
   sat on top of the ranking and why nothing could live below the frame. */
.rs-board { margin: 0 0 18px; }
.rs-frame {
  width: 100%;
  height: clamp(520px, 78vh, 980px);
  border: 1px solid var(--hairline);
  border-radius: 10px;
  background: var(--bg-raised);
  display: block;
}

/* --- method -------------------------------------------------------------- */
/* Everything that used to be above the fold. A reader who wants it can have
   all of it; a reader who wants the ranking is no longer made to read it. */
.rs-method {
  border-top: 1px solid var(--hairline);
  padding-top: 10px;
  margin: 0 0 28px;
}
.rs-method > summary {
  cursor: pointer;
  list-style: none;
  font-size: var(--fs-sm);
  color: var(--text-3);
  display: flex;
  align-items: baseline;
  gap: 7px;
}
.rs-method > summary::-webkit-details-marker { display: none; }
.rs-method > summary::before {
  content: "\25B8";
  display: inline-block;
  transition: transform 120ms var(--ease);
}
.rs-method[open] > summary::before { transform: rotate(90deg); }
.rs-method > summary:hover { color: var(--text); }
.rs-method > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.rs-method-body {
  max-width: 72ch;
  margin-top: 10px;
  color: var(--text-2);
  font-size: var(--fs-sm);
  line-height: 1.6;
}
.rs-method-body p { margin: 0 0 9px; }
.rs-method-body strong { color: var(--text); }

@media (prefers-reduced-motion: reduce) {
  .rs-method > summary::before { transition: none; }
}

@media (max-width: 720px) {
  .rs-tabs { grid-template-columns: 1fr; gap: 8px; }
  .rs-tab .rs-ask { display: none; }
  .rs-stat { min-width: 0; flex: 1 1 calc(50% - 5px); }
  .rs-stat dd { font-size: 22px; }
  .rs-table { font-size: var(--fs-sm); }
  /* Targets/gm is the most droppable column: target share says the same thing
     in the form people actually quote. */
  .rs-table th:nth-child(5), .rs-table td:nth-child(5) { display: none; }
  .rs-frame { height: 70vh; }
}
