/* ------------------------------------------------------------------ */
/* Design tokens                                                        */
/* ------------------------------------------------------------------ */
:root {
  --bg:           #0c0e14;
  --surface:      #13151f;
  --surface-2:    #1a1d2b;
  --surface-3:    #20233a;
  --border:       #252838;
  --border-focus: #3b4060;

  --text:         #e2e5f0;
  --text-2:       #a0a8bc;
  /* Tertiary/muted text. Lightened from #5c647a so small muted labels,
     captions, placeholders, and "n/a" states clear WCAG AA (>=4.5:1) on
     every surface (--bg/--surface/--surface-2/--surface-3); still clearly
     dimmer than --text-2, so the text > text-2 > text-3 hierarchy holds. */
  --text-3:       #858ea6;

  --green:        #22c55e;
  --green-dim:    rgba(34,197,94,.12);
  --red:          #ef4444;
  --red-dim:      rgba(239,68,68,.12);
  --amber:        #f59e0b;
  --amber-dim:    rgba(245,158,11,.12);
  --blue:         #60a5fa;
  --blue-dim:     rgba(96,165,250,.12);
  --purple:       #a78bfa;
  --purple-dim:   rgba(167,139,250,.12);

  /* Accent border-emphasis variants (stronger than the .12 -dim fills),
     used for state borders such as toasts. */
  --green-border: rgba(34,197,94,.3);
  --red-border:   rgba(239,68,68,.3);
  --blue-border:  rgba(96,165,250,.3);

  /* Primary-button blue accents, sitting between -dim (.12) and -border (.3). */
  --blue-border-soft: rgba(96,165,250,.25);
  --blue-fill-strong: rgba(96,165,250,.2);

  /* Modal scrim (dim the page behind an open dialog). */
  --overlay: rgba(0,0,0,.65);

  --font-mono: "SF Mono","Cascadia Code","Fira Code","Menlo",monospace;
  --font-ui:   -apple-system,BlinkMacSystemFont,"Segoe UI","Helvetica Neue",sans-serif;

  --radius-sm: 4px;
  --radius:    6px;
  --radius-lg: 10px;
}

/* ------------------------------------------------------------------ */
/* Reset                                                               */
/* ------------------------------------------------------------------ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* Opt the whole document into the dark UA palette so native chrome matches the
   dark theme: form-control widgets (the datetime-local / number / file inputs
   and select dropdowns in the Log/Settle modals), checkboxes, spinners, and
   scrollbars render dark instead of the default light, and the pre-CSS canvas
   paints dark. Keyword, not a color literal — no design token applies. */
/* scrollbar-gutter reserves the classic-scrollbar strip permanently, so locking
   the page behind an open dialog (html.modal-open, in the Modal section) cannot
   reclaim it and shift the whole dashboard sideways under the translucent scrim.
   No-op where scrollbars overlay the content (macOS, iOS) and harmlessly ignored
   by engines without it — those keep today's behaviour, nothing regresses. */
html { font-size: 14px; color-scheme: dark; scrollbar-gutter: stable; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  line-height: 1.5;
  min-height: 100vh;
}
a { color: var(--blue); text-decoration: none; }
button { cursor: pointer; font: inherit; border: none; background: none; }

/* Keyboard focus ring. Shown only for keyboard navigation (not mouse clicks),
   so the custom-styled buttons, links, checkboxes, and <summary> disclosure
   widgets get a consistent, visible focus affordance instead of relying on the
   default UA outline (which is near-invisible on this dark theme). Inputs/selects
   share the same ring for keyboard focus (see the input rules below) and keep
   the subtler border-color change for pointer focus. */
button:focus-visible,
a:focus-visible,
summary:focus-visible,
[type="checkbox"]:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* "Skip to content" link (WCAG 2.4.1 Bypass Blocks). Off-screen until it takes
   keyboard focus, then pinned top-left above the sticky header so a keyboard or
   screen-reader user can jump past the ~20 nav + toolbar controls straight to
   the dashboard content, instead of tabbing through every one first. It is the
   first focusable element on the page; pointer users never see it. */
.skip-link {
  position: fixed;
  top: 8px;
  left: 8px;
  z-index: 1000;
  padding: 8px 14px;
  background: var(--surface-2);
  color: var(--text);
  border: 1px solid var(--border-focus);
  border-radius: var(--radius);
  transform: translateY(-150%);
  opacity: 0;
  pointer-events: none;
}
.skip-link:focus {
  transform: translateY(0);
  opacity: 1;
  pointer-events: auto;
}
/* The skip target is a landmark container, not a control: suppress the focus
   ring the -1 tabindex would otherwise draw around the whole content region
   (the sticky-header offset when jumped to lives on the .content layout rule). */
.content:focus { outline: none; }

/* Respect the OS "reduce motion" accessibility setting: neutralize the
   decorative entrance animations (toast-in, fade-in, slide-up) and UI
   transitions for users who opt out of motion. The loading spinner is kept
   animating on purpose — it is the one essential in-flight indicator (the
   Import button shows it alone, with no text), so stopping it would hide that
   work is still in progress. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .spinner { animation: spin 0.6s linear infinite !important; }
}

/* ------------------------------------------------------------------ */
/* Layout                                                              */
/* ------------------------------------------------------------------ */
#app { display: flex; flex-direction: column; min-height: 100vh; }

/* Two-level nav (#253): the header is two rows now — brand, the three section
   chips and the actions on row 1; the active section's sub-nav, full width, on
   row 2. It wraps to produce that second row, so it is no longer a fixed-height
   single row: row 1's height comes from .nav-groups, row 2's from .nav. */
.header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0 24px;
  padding: 0 24px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  position: sticky;
  top: 0;
  z-index: 100;
  flex-shrink: 0;
}

.logo {
  font-family: var(--font-mono);
  font-size: 16px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: -0.5px;
  flex-shrink: 0;
}
.logo span { color: var(--purple); }

/* Product switch — toobsox is a shell over two products (edge, fantasy) and
   this is the one control that crosses between them. Real links, not buttons:
   they are separate pages, so Back works and a middle-click opens the other
   product in a tab. Styled off the same tokens as .nav-group-btn so the two
   rows of header chrome read as one system, but visually quieter — switching
   product is rarer than switching section. Issue 308 (F0). */
.product-switch {
  display: flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}
.product-link {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-3);
  text-decoration: none;
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
}
.product-link:hover { color: var(--text); background: var(--surface-2); }
.product-link.active {
  color: var(--purple);
  background: var(--purple-dim);
  border-color: var(--border);
}

/* Top level: three section chips, which is few enough to fit a phone without
   scrolling — the whole point of the split. Sits on header row 1 where the
   15-tab strip used to be. */
.nav-groups {
  display: flex;
  align-items: center;
  gap: 4px;
  /* Grows into the space the 15-tab strip used to hold, but never shrinks
     below its own width: at a basis of 0 (the old strip's `flex: 1`) a narrow
     desktop window squeezed the nav to nothing and the chips overflowed under
     the action buttons. Held at content width, the actions wrap to a row of
     their own instead — which the header now supports. */
  flex: 1 0 auto;
  min-height: 52px;
}

.nav-group-btn {
  padding: 6px 14px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
  transition: color 0.15s, background 0.15s;
}
.nav-group-btn:hover { color: var(--text); background: var(--surface-2); }
/* Purple fill rather than the sub-nav's neutral --surface-3, so the two levels
   are told apart at a glance and the active section reads as the stronger of
   the two. */
.nav-group-btn.active {
  color: var(--text);
  background: var(--purple-dim);
}

/* Second level: the active section's views, on their own full-width row.
   Full-bleed via negative margins so the divider meets both header edges. It
   is also the non-scrolling positioning context for the strip's edge fade
   below the phone breakpoint (see the responsive block at the end of this
   file), where the six-view sections can still overflow. */
.nav-scroll {
  /* Basis is 100% *plus* the header's horizontal padding, which the negative
     margins then cancel: a plain 100% basis measures the padding box, so the
     row — and its divider — would stop 24px short of each header edge. Larger
     than the line either way, so it always wraps onto a row of its own. */
  flex: 0 0 calc(100% + 48px);
  min-width: 0;
  position: relative;
  margin: 0 -24px;
  padding: 0 24px;
  border-top: 1px solid var(--border);
}

.nav {
  display: flex;
  align-items: center;
  gap: 2px;
  min-height: 40px;
}

.nav-btn {
  padding: 6px 14px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  color: var(--text-2);
  transition: color 0.15s, background 0.15s;
}
.nav-btn:hover { color: var(--text); background: var(--surface-2); }
.nav-btn.active {
  color: var(--text);
  background: var(--surface-3);
}

.header-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }

/* ------------------------------------------------------------------ */
/* Buttons                                                             */
/* ------------------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  font-weight: 500;
  transition: opacity 0.15s, background 0.15s;
}
.btn:disabled { opacity: 0.45; cursor: not-allowed; }
.btn-primary {
  background: var(--blue-dim);
  color: var(--blue);
  border: 1px solid var(--blue-border-soft);
}
.btn-primary:hover:not(:disabled) { background: var(--blue-fill-strong); }

.btn-ghost {
  background: transparent;
  color: var(--text-2);
  border: 1px solid var(--border);
}
.btn-ghost:hover:not(:disabled) { background: var(--surface-2); color: var(--text); }

.btn-sm { padding: 4px 10px; font-size: 12px; }

/* ------------------------------------------------------------------ */
/* Stats bar                                                           */
/* ------------------------------------------------------------------ */
.stats-bar {
  display: flex;
  gap: 1px;
  background: var(--border);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.stat-card {
  flex: 1;
  background: var(--surface);
  padding: 14px 20px;
  min-width: 120px;
}
.stat-label {
  font-size: 11px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
  margin-bottom: 4px;
}
.stat-value {
  font-family: var(--font-mono);
  font-size: 20px;
  font-weight: 600;
  color: var(--text);
  line-height: 1.2;
}
.stat-value.pos { color: var(--green); }
.stat-value.neg { color: var(--red); }
.stat-value.muted { color: var(--text-3); }
.stat-sub {
  font-size: 11px;
  color: var(--text-3);
  margin-top: 2px;
}

/* ------------------------------------------------------------------ */
/* Market band                                                         */
/* ------------------------------------------------------------------ */
/* The Market group's half of the contextual band (#259): snapshot provenance
   where the Ledger group has its money. Deliberately NOT built out of
   .stat-card — the five-card bar is the thing #259 exists to get off these
   views, and repeating its ~76px (two wrapped rows at 375px) with different
   numbers in it would spend the space it just reclaimed. One line of
   label/value pairs, ~33px, wrapping instead of scrolling. */
.market-band {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 24px;
  padding: 8px 24px;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.market-band-item {
  display: flex;
  align-items: baseline;
  gap: 8px;
  min-width: 0;
}
.market-band-label {
  font-size: 10px;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
  white-space: nowrap;
}
.market-band-value {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-2);
  overflow-wrap: anywhere;
}

/* ------------------------------------------------------------------ */
/* Content area                                                        */
/* ------------------------------------------------------------------ */
/* scroll-margin-top clears the sticky header for the skip-to-content jump:
   52px of row 1 plus the 41px sub-nav row the two-level nav adds (#253), and
   another 52px for the narrow-desktop window where the action buttons wrap to
   a row of their own. Overshooting on a wider window only reveals a little of
   the stats bar above the content, which is harmless. */
.content { flex: 1; padding: 24px; overflow: auto; scroll-margin-top: 150px; }

/* ------------------------------------------------------------------ */
/* Tables                                                              */
/* ------------------------------------------------------------------ */
.table-wrap { overflow-x: auto; }

/* A wide table scrolls sideways here, which a pointer can drag but a keyboard
   cannot reach — a <div> takes no focus, so the arrow keys that would scroll
   it never arrive (WCAG 2.1.1). markScrollableTables() — in app.js for /edge
   and in fantasy.js for /fantasy — gives the wrap a tabindex while it is
   actually overflowing; this is the ring that tab stop shows, matched to the
   global focus-visible treatment above. Keyboard only: a click on a cell must
   not draw a box round the whole grid. */
.table-wrap:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

thead th {
  text-align: left;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
  padding: 8px 12px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  background: var(--surface);
}
thead th.r { text-align: right; }

tbody tr {
  border-bottom: 1px solid var(--border);
  transition: background 0.1s;
}
tbody tr:hover { background: var(--surface); }
tbody tr:last-child { border-bottom: none; }

td {
  padding: 9px 12px;
  color: var(--text);
  vertical-align: middle;
  white-space: nowrap;
}
td.r { text-align: right; font-family: var(--font-mono); }
td.muted { color: var(--text-2); }
td.dim { color: var(--text-3); font-size: 12px; }
/* The same de-emphasis inside a cell rather than on it (F7b): the start/sit
   board puts a player's position and the reason he has no projection beside his
   name, and both are context for the name rather than data of their own. Same
   token as td.dim; the cell keeps its own size so a wrapped line does not
   shrink mid-row. */
span.dim { color: var(--text-3); }
td.mono { font-family: var(--font-mono); }

/* ------------------------------------------------------------------ */
/* Badges                                                              */
/* ------------------------------------------------------------------ */
.badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  font-size: 11px;
  font-weight: 600;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.badge-win    { background: var(--green-dim); color: var(--green); }
.badge-loss   { background: var(--red-dim);   color: var(--red);   }
.badge-open   { background: var(--amber-dim); color: var(--amber); }
.badge-push   { background: var(--surface-3); color: var(--text-2);}
.badge-void   { background: var(--surface-3); color: var(--text-3);}

/* ------------------------------------------------------------------ */
/* Bets tab — search/filter/sort (#254)                                */
/* ------------------------------------------------------------------ */
.bets-filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 10px;
}
.bets-filter-bar input[type="text"],
.bets-filter-bar input[type="date"],
.bets-filter-bar select {
  width: auto;
  min-width: 110px;
}
#bets-filter-search { min-width: 200px; flex: 1 1 200px; }

.bets-filter-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.bets-filter-chip {
  padding: 5px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-2);
  font: inherit;
  font-size: 12px;
  font-weight: 500;
  cursor: pointer;
}
.bets-filter-chip:hover { background: var(--surface-3); color: var(--text); }
.bets-filter-chip.active { background: var(--blue-dim); color: var(--blue); border-color: var(--blue-border-soft); }

.bets-filter-summary {
  font-size: 12px;
  color: var(--text-3);
  margin-bottom: 8px;
}

.bets-sort-btn {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  color: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}
.bets-sort-btn:hover { color: var(--text); }

.empty-cell {
  text-align: center;
  color: var(--text-3);
  padding: 20px 12px;
  white-space: normal;
}

/* ------------------------------------------------------------------ */
/* Cards (events on lines tab)                                         */
/* ------------------------------------------------------------------ */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  margin-bottom: 16px;
  overflow: hidden;
}

.card-header {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 14px 20px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.card-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
}
.card-meta {
  font-size: 12px;
  color: var(--text-3);
}
.card-sport {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
  padding: 2px 7px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* Lines table within a card */
.lines-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.lines-table th {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-3);
  padding: 8px 14px;
  text-align: right;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
  background: var(--surface);
}
.lines-table th:first-child { text-align: left; }
.lines-table td {
  padding: 9px 14px;
  text-align: right;
  font-family: var(--font-mono);
  font-size: 13px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
.lines-table td:first-child { text-align: left; font-family: var(--font-ui); color: var(--text-2); }
.lines-table tr:last-child td { border-bottom: none; }
.lines-table tr:hover td { background: var(--surface-2); }

.price-best {
  color: var(--green);
  font-weight: 700;
}
.price-na { color: var(--text-3); }
.price-benchmark { color: var(--text-3); }

/* Lines: the no-vig Fair column (#256), off by default. Muted like the
   benchmark it is derived from, with the fair probability as a smaller second
   line so the column stays one column wide. */
.price-fair { color: var(--text-2); }
.price-fair-pct {
  display: block;
  font-size: 11px;
  color: var(--text-3);
}

.market-tab-bar {
  display: flex;
  gap: 1px;
  padding: 0 20px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.market-tab {
  padding: 8px 14px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-3);
  border-bottom: 2px solid transparent;
  cursor: default;
}

/* ------------------------------------------------------------------ */
/* Market views — shared Lines/Plays filter bar (#255)                  */
/*                                                                      */
/* Sport/book chips, market + time-window selects, search, and the      */
/* "showing X of Y" count. Chips borrow the nav-button idiom (neutral   */
/* --surface-3 fill when on) so the dashboard reads as one system.      */
/* ------------------------------------------------------------------ */
.market-filter-bar {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 12px 14px;
  margin-bottom: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.mf-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.mf-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
}
/* Grows into the row; the basis keeps it usable once the row wraps. */
.mf-search { flex: 1 1 200px; min-width: 0; }
/* Override the shared form-control width:100% — these sit inline. */
.mf-select { flex: 0 0 auto; width: auto; }
.mf-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.mf-chip {
  padding: 5px 11px;
  font-size: 12px;
  font-weight: 600;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text-3);
  transition: color 0.15s, background 0.15s, border-color 0.15s;
}
.mf-chip:hover { color: var(--text); border-color: var(--border-focus); }
.mf-chip.active {
  background: var(--surface-3);
  border-color: var(--border-focus);
  color: var(--text);
}
.mf-summary {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-3);
}

/* A Lines event card's header doubles as its disclosure control, so a full
   slate opens navigable. Collapsed, the header is the whole card — its
   divider would otherwise underline nothing. */
.card-header-toggle {
  width: 100%;
  text-align: left;
}
.card-header-toggle:hover { background: var(--surface-3); }
.card-header-toggle[aria-expanded="false"] { border-bottom: none; }
.mf-caret { font-size: 10px; color: var(--text-3); }

/* Lines tab — per-selection price-history sparkline (P4) */
.line-hist-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--text-2);
  cursor: pointer;
  text-align: left;
}
.line-hist-toggle:hover { color: var(--text); }
.line-hist-toggle.open { color: var(--blue); }
.line-hist-caret { font-size: 10px; color: var(--text-3); }
.line-hist-toggle.open .line-hist-caret { color: var(--blue); }

.line-hist-row td { padding: 0; background: var(--surface-2); }
.line-hist-panel { padding: 14px 20px 16px; }
.line-hist-msg {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text-3);
}

.line-hist-legend {
  display: flex;
  gap: 18px;
  margin-bottom: 8px;
  font-size: 11px;
  color: var(--text-2);
}
.line-hist-key { display: inline-flex; align-items: center; gap: 6px; }
.line-hist-swatch {
  width: 12px;
  height: 3px;
  border-radius: 2px;
  display: inline-block;
}
.line-hist-now { font-family: var(--font-mono); color: var(--text); }
.line-hist-axis-note {
  margin-top: 4px;
  font-size: 10px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ------------------------------------------------------------------ */
/* Edge tab                                                            */
/* ------------------------------------------------------------------ */
.section-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}
.section-title {
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-3);
}
.section-count {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  padding: 2px 7px;
  border-radius: var(--radius-sm);
  background: var(--purple-dim);
  color: var(--purple);
}

/* ------------------------------------------------------------------ */
/* Plays: Kelly stake sizing, relocated from the retired Stakes tab (#256) */
/*                                                                        */
/* The bankroll override sits in the +EV section header; the exposure line */
/* replaces what used to be a whole stat bar on a page of its own.         */
/* ------------------------------------------------------------------ */
.plays-bankroll {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 6px;
}
.plays-bankroll label {
  font-weight: 500;
  color: var(--text-3);
  white-space: nowrap;
}
/* Override the shared form-control width:100% — this sits inline. Attribute
   selector included so it outranks the later `input[type="number"]` block;
   font-size is deliberately left alone so the mobile 16px bump (iOS focus
   zoom) and the 44px touch target still reach it. */
.plays-bankroll input[type="number"] {
  width: 100px;
  padding: 4px 6px;
}

.stake-summary {
  font-size: 12px;
  color: var(--text-2);
  margin: -4px 0 12px;
  line-height: 1.6;
}
.stake-summary strong { color: var(--text); font-family: var(--font-mono); }

.stake-cap {
  font-family: var(--font-ui);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
}

.ev-pct {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--green);
}

.arb-profit {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--purple);
}

/* ------------------------------------------------------------------ */
/* Misc / States                                                       */
/* ------------------------------------------------------------------ */
.loading-init {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
  font-size: 13px;
  color: var(--text-3);
  font-family: var(--font-mono);
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 80px 24px;
  text-align: center;
}
.empty-state-icon {
  font-size: 32px;
  margin-bottom: 16px;
  opacity: 0.5;
}
.empty-state-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text-2);
  margin-bottom: 8px;
}
.empty-state-body {
  font-size: 13px;
  color: var(--text-3);
  max-width: 320px;
  line-height: 1.6;
}

.toast-bar {
  position: fixed;
  bottom: 24px;
  right: 24px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 1000;
}
.toast {
  padding: 10px 16px;
  border-radius: var(--radius);
  font-size: 13px;
  font-weight: 500;
  max-width: 360px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  animation: toast-in 0.2s ease;
}
.toast.toast-ok    { border-color: var(--green-border); color: var(--green); }
.toast.toast-err   { border-color: var(--red-border);   color: var(--red);   }
.toast.toast-info  { border-color: var(--blue-border);  color: var(--blue);  }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}

.pnl-pos { font-family: var(--font-mono); color: var(--green); }
.pnl-neg { font-family: var(--font-mono); color: var(--red);   }
.pnl-zero { font-family: var(--font-mono); color: var(--text-3); }

.clv-pos { font-family: var(--font-mono); color: var(--green); font-size: 12px; }
.clv-neg { font-family: var(--font-mono); color: var(--red);   font-size: 12px; }
.clv-na  { font-family: var(--font-mono); color: var(--text-3); font-size: 12px; }

.spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
  vertical-align: -2px;
}
@keyframes spin { to { transform: rotate(360deg); } }

.pulled-at {
  font-size: 12px;
  color: var(--text-3);
  margin-bottom: 20px;
  font-family: var(--font-mono);
}

.section-gap { margin-top: 32px; }

/* ------------------------------------------------------------------ */
/* Charts                                                              */
/* ------------------------------------------------------------------ */
.charts-row {
  display: flex;
  gap: 16px;
  margin-bottom: 24px;
  flex-wrap: wrap;
}

.chart-card {
  flex: 1;
  min-width: 280px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 16px 20px 12px;
}

.chart-title {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--text-3);
  margin-bottom: 10px;
}

.chart-svg {
  width: 100%;
  height: auto;
  display: block;
  overflow: visible;
}

.chart-label {
  font-family: var(--font-mono);
  font-size: 9px;
  fill: var(--text-3);
}

/* ------------------------------------------------------------------ */
/* Modal                                                               */
/* ------------------------------------------------------------------ */
/* Scroll lock. Both dialogs are aria-modal="true", which tells a screen reader
   the rest of the page is inert — but the page behind stayed scrollable, so a
   wheel over the scrim, or a touch-drag continuing past the end of the modal's
   own scroll, dragged the ledger around underneath and left the user somewhere
   else entirely once the dialog closed. Locking the root element is what the
   aria-modal claim already implies. The class is toggled in render() from
   S.modal/S.settleModal, so it clears on whichever path closes the dialog
   (Escape, Cancel, scrim click, submit). Scroll POSITION survives the lock —
   the offset is retained, the box just stops scrolling — so the view is
   unchanged when the dialog goes away. The overlay is position:fixed and so is
   unaffected by it. */
html.modal-open { overflow: hidden; }

.modal-overlay {
  position: fixed;
  inset: 0;
  background: var(--overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 500;
  animation: fade-in 0.15s ease;
}
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }

.modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  width: 100%;
  max-width: 520px;
  max-height: 90vh;
  overflow-y: auto;
  /* The dialog is its own scroll container (a tall Log Bet form on a phone
     overflows 90vh). Contain the chaining so a flick that reaches the end of
     the form stops there instead of handing the rest of the gesture to the
     page behind — the root lock above covers the document scroller, this
     covers the nested one. */
  overscroll-behavior: contain;
  animation: slide-up 0.15s ease;
}
@keyframes slide-up { from { transform: translateY(12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 24px 16px;
  border-bottom: 1px solid var(--border);
}
.modal-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
}
.modal-close {
  font-size: 16px;
  color: var(--text-3);
  cursor: pointer;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  transition: color 0.1s, background 0.1s;
}
.modal-close:hover { color: var(--text); background: var(--surface-2); }

/* ------------------------------------------------------------------ */
/* Form                                                                */
/* ------------------------------------------------------------------ */
.bet-form { padding: 20px 24px 24px; }

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 14px;
}

.form-group {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin-bottom: 14px;
}
.form-group:last-of-type { margin-bottom: 0; }

label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-2);
}
.label-hint {
  font-weight: 400;
  color: var(--text-3);
}

input[type="text"],
input[type="number"],
input[type="datetime-local"],
select {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  padding: 7px 10px;
  width: 100%;
  outline: none;
  transition: border-color 0.15s;
  -webkit-appearance: none;
}
input[type="text"]:focus,
input[type="number"]:focus,
input[type="datetime-local"]:focus,
select:focus { border-color: var(--blue); }

/* Keyboard focus on form fields gets the same 2px ring as buttons/links, so
   tabbing through inputs and selects is as clearly indicated as the other
   controls. Pointer focus keeps just the subtle border-color change above. */
input[type="text"]:focus-visible,
input[type="number"]:focus-visible,
input[type="datetime-local"]:focus-visible,
select:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

/* Same treatment as the shared block above, kept as its own rule (rather
   than folded into it) so it doesn't shift the literal selector chain
   tests/test_mobile_forms.py pins for the #205 mobile breakpoint. */
input[type="date"] {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  padding: 7px 10px;
  width: 100%;
  outline: none;
  transition: border-color 0.15s;
  -webkit-appearance: none;
}
input[type="date"]:focus { border-color: var(--blue); }
input[type="date"]:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}

input::placeholder { color: var(--text-3); }

select { cursor: pointer; }
select option { background: var(--surface-2); }

.input-prefix-wrap {
  position: relative;
  display: flex;
  align-items: center;
}
.input-prefix {
  position: absolute;
  left: 10px;
  font-size: 13px;
  color: var(--text-3);
  pointer-events: none;
}
.input-prefix-wrap input { padding-left: 22px; }

.form-error {
  min-height: 18px;
  font-size: 12px;
  color: var(--red);
  margin: 10px 0 4px;
}

.form-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

/* ------------------------------------------------------------------ */
/* Log Bet: game picker + guided odds entry (#415)                      */
/* ------------------------------------------------------------------ */
/* An optional block above the form proper, set apart on its own surface so
   it reads as "pick, and the fields below fill in" rather than as four more
   fields to complete. Tokens only. */
.bet-picker {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 12px 14px;
  margin-bottom: 16px;
}
/* Matches the <label> treatment — it heads the group the same way. */
.pick-title,
.pick-books-label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-2);
}
.pick-title { margin-bottom: 10px; }

/* Inside the picker the paired selects sit on --surface-2 already, so the
   controls take the next surface up to stay distinguishable from it. */
.bet-picker select { background: var(--surface-3); }

.pick-books {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.pick-book {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text-2);
  font-size: 12px;
  padding: 6px 9px;
  transition: border-color 0.15s, background 0.15s, color 0.15s;
}
.pick-book:hover { border-color: var(--border-focus); color: var(--text); }
.pick-book.selected {
  border-color: var(--blue);
  background: var(--blue-dim);
  color: var(--text);
}
.pick-book-odds {
  font-family: var(--font-mono);
  color: var(--text);
}
/* The best held price is marked by the "best" tag as well as the colour, so
   the comparison still reads without relying on hue alone. */
.pick-book-best .pick-book-odds { color: var(--green); font-weight: 700; }
.pick-book-tag {
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--green);
  background: var(--green-dim);
  border-radius: var(--radius-sm);
  padding: 1px 4px;
}

.pick-note {
  font-size: 12px;
  color: var(--text-3);
}
/* The note that stands in for the whole picker when there is nothing to pick. */
.pick-note-block {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 10px 12px;
  margin-bottom: 16px;
}

/* The odds read-back, directly under the odds input inside its form group. */
.odds-echo {
  font-size: 12px;
  color: var(--text-2);
  min-height: 16px;
}
.odds-echo-error { color: var(--red); }

/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--border-focus); }

/* ------------------------------------------------------------------ */
/* Inline table-row action buttons: Settle and Edit (bets table result */
/* cell) and Track (Edge/Lines rows — prefill the log-bet modal)       */
/* ------------------------------------------------------------------ */
.settle-btn,
.edit-btn,
.track-btn {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 2px 7px;
  margin-left: 6px;
  vertical-align: middle;
  transition: color 0.12s, border-color 0.12s, background 0.12s;
}
.settle-btn:hover,
.edit-btn:hover,
.track-btn:hover {
  color: var(--text);
  border-color: var(--border-focus);
  background: var(--surface-2);
}

/* Track in a dedicated trailing cell: shrink the column, drop the
   inline-text offset (the offset stays for the soft-arb leg cells).   */
.track-cell { width: 1%; white-space: nowrap; }
.track-cell .track-btn { margin-left: 0; }

/* Settle modal bet-info summary row */
.settle-bet-info {
  display: flex;
  gap: 12px;
  align-items: center;
  flex-wrap: wrap;
  padding: 12px 24px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
  background: var(--surface-2);
}

/* ------------------------------------------------------------------ */
/* Config tab                                                          */
/* ------------------------------------------------------------------ */
.config-wrap {
  max-width: 560px;
  padding: 24px 0;
  display: flex;
  flex-direction: column;
  gap: 32px;
}

.config-section {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.config-section-title {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
}

.config-section-sub {
  font-size: 12px;
  color: var(--text-3);
  margin-top: -6px;
}

/* System Status card (issue 175) — the Config tab's "did this deploy come up
   wired" read-out. Deliberately quiet: every row states a fact, and only the
   genuinely actionable ones (no odds key, a stale snapshot) carry a warn tone.
   Auth "off" and flat-file storage are legitimate postures, so they read as
   neutral, not as alarms. Tokens only — no raw colors. */
.sys-status-rows {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.sys-status-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
}
.sys-status-row:last-child { border-bottom: none; }

.sys-status-label {
  font-size: 12px;
  color: var(--text-2);
}

.sys-status-value {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text);
  text-align: right;
}

/* The explanatory line under a row that needs one (e.g. "Lines and Edge stay
   empty until ODDS_API_KEY is set"). Sits inside the bordered list, indented to
   read as belonging to the row above it. */
.sys-status-note {
  padding: 0 12px 9px;
  margin-top: -4px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-3);
  border-bottom: 1px solid var(--border);
}
.sys-status-note:last-child { border-bottom: none; }

.sys-status-pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  font-size: 11px;
  font-weight: 600;
  white-space: nowrap;
}
.sys-status-pill-ok   { background: var(--green-dim); color: var(--green); }
.sys-status-pill-warn { background: var(--amber-dim); color: var(--amber); }
.sys-status-pill-idle { background: var(--surface-3); color: var(--text-3); }
/* The "failed" tone (issue 271). Completes the family above rather than
   starting a second one: the Ledger backup card needs a genuinely bad state (a
   run that failed outright), where System Status only ever needed
   ok / warn / neutral. A stale backup deliberately does NOT use this — that is
   a warning (ratified call (b)), not an error. */
.sys-status-pill-bad  { background: var(--red-dim); color: var(--red); }

.sys-status-release {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-3);
  background: var(--surface-2);
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  white-space: nowrap;
}

/* ------------------------------------------------------------------ */
/* Ledger backup card (issue 271)                                      */
/* ------------------------------------------------------------------ */
/* The Health tab's read-out for the off-box copies scripts/backup.py makes.
   Reuses the .sys-status-* rows and pills above verbatim (and the shared
   snapshot chip below for age), so everything here is just the card wrapper
   around them. Tokens only — no raw colors. */
.backup-card-body {
  padding: 14px 20px;
}

/* Loading / could-not-read line inside the card body. */
.backup-card-msg {
  font-size: 12px;
  color: var(--text-3);
}

/* The absolute timestamp beside a relative age or an outcome pill. Quiet and
   monospaced: the relative age is the answer, this is the receipt. */
.backup-stamp {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-3);
  white-space: nowrap;
}

/* "No backup run recorded" — a statement about the record, styled as calm
   prose rather than an alarm or a reassuring green. */
.backup-empty-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
  margin-bottom: 4px;
}

.backup-empty-body {
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-3);
}

/* Snapshot freshness chip — the shared two-tier treatment (#176's ratified
   rule). "fresh" renders no chip at all; a current snapshot is the unremarkable
   case. Every consumer of snapshotBadge() styles through these three. */
.snapshot-badge {
  display: inline-block;
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.snapshot-badge-stale   { background: var(--red-dim);   color: var(--red);    }
.snapshot-badge-aging   { background: var(--amber-dim); color: var(--amber);  }
.snapshot-badge-unknown { background: var(--surface-3); color: var(--text-3); }

/* The loud half of the freshness read-out on the price tabs (issue 176). A 10px
   chip is easy to miss above a dense price grid, and hours-old prices
   rendering as current is the failure the whole guard exists to prevent — so
   a stale snapshot also says so in words, directly above the price grid.
   Since #259 moved the timestamp into the Market band, the banner is usually
   the first thing in the tab body and stands on its own margin; the pull-up
   that made it read as one block with a preceding line is now conditional on
   there actually being one (Lines' de-vig note is the remaining case). */
.snapshot-warning {
  margin: 0 0 20px;
  padding: 10px 12px;
  border: 1px solid var(--red-border);
  border-radius: var(--radius);
  background: var(--red-dim);
  color: var(--text-2);
  font-size: 12px;
  line-height: 1.5;
}
.pulled-at + .snapshot-warning { margin-top: -12px; }
/* An unreadable timestamp is an unknown, not a fault: same shape, neutral tone
   (the same distinction .snapshot-badge-unknown draws against -stale). */
.snapshot-warning-unknown {
  border-color: var(--border);
  background: var(--surface-2);
}

/* In-play marker (issue 176's ratified call (b)) — a play whose event has started
   since the snapshot was pulled. Visible and clearly marked, never hidden:
   amber, because this is a "not what you think it is" state rather than an
   error. The row itself dims to --text-3 so an in-play price cannot be
   mistaken at a glance for a live pre-match edge. */
.live-badge {
  display: inline-block;
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  background: var(--amber-dim);
  color: var(--amber);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
}

.live-section-note {
  font-size: 12px;
  color: var(--text-3);
  line-height: 1.5;
  max-width: 640px;
  margin: -4px 0 12px;
}

.play-live-row td { color: var(--text-3); }

.book-list {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.book-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 14px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}
.book-item:last-child { border-bottom: none; }

.book-item-id {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--purple);
  min-width: 100px;
}
.book-item-name { flex: 1; }
.book-item-region {
  font-size: 11px;
  font-family: var(--font-mono);
  min-width: 30px;
}

.book-add-form {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  padding: 8px 0 0;
}
.book-add-form input,
.book-add-form select {
  flex: 1;
  min-width: 100px;
  width: auto;
}
.book-add-form .btn { flex-shrink: 0; }

.config-actions {
  display: flex;
  gap: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

/* ------------------------------------------------------------------ */
/* Sports picker                                                        */
/* ------------------------------------------------------------------ */
.sport-groups {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 16px;
}

.sport-group { display: flex; flex-direction: column; gap: 4px; }

.sport-group-label {
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-3);
  margin-bottom: 4px;
}

.sport-group-rows { display: flex; flex-direction: column; gap: 2px; }

.sport-checkbox-row {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: var(--radius-sm);
  transition: background 0.1s;
}
.sport-checkbox-row:hover { background: var(--surface-2); }

.sport-checkbox-row input[type="checkbox"] {
  width: 14px;
  height: 14px;
  min-width: 14px;
  accent-color: var(--purple);
  cursor: pointer;
  padding: 0;
}

.sport-title {
  font-size: 13px;
  display: flex;
  align-items: center;
  gap: 5px;
}

.sport-active-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--green);
  flex-shrink: 0;
}

/* ------------------------------------------------------------------ */
/* Coverage tab                                                        */
/* ------------------------------------------------------------------ */
.cov-pct-ok   { font-family: var(--font-mono); color: var(--green); }
.cov-pct-low  { font-family: var(--font-mono); color: var(--amber); }
.cov-pct-zero { font-family: var(--font-mono); color: var(--red); }

.cov-chip {
  display: inline-block;
  padding: 2px 7px;
  border-radius: var(--radius-sm);
  background: var(--red-dim);
  color: var(--red);
  font-size: 11px;
  font-family: var(--font-mono);
  margin-right: 4px;
}

.cov-gaps { margin-bottom: 24px; }

.cov-gap-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 10px 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 8px;
  font-size: 13px;
}

.cov-gap-label { font-weight: 600; color: var(--text); }

/* ------------------------------------------------------------------ */
/* Bets tab — aggregate CLV curve (P4)                                  */
/* ------------------------------------------------------------------ */
.clv-curve-card { margin-bottom: 24px; }

.clv-curve-msg {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text-3);
  padding: 4px 0 8px;
}

.clv-curve-legend {
  display: flex;
  gap: 18px;
  margin-bottom: 8px;
  font-size: 11px;
  color: var(--text-2);
}
.clv-curve-key { display: inline-flex; align-items: center; gap: 6px; }
.clv-curve-swatch {
  width: 12px;
  height: 3px;
  border-radius: 2px;
  display: inline-block;
}
.clv-curve-now { font-family: var(--font-mono); }

.clv-curve-axis-note {
  margin-top: 4px;
  font-size: 10px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ------------------------------------------------------------------ */
/* Bets tab — per-bet CLV timeline (P4)                                 */
/* ------------------------------------------------------------------ */
.bet-timeline-toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: none;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--text-2);
  cursor: pointer;
  text-align: left;
}
.bet-timeline-toggle:hover { color: var(--text); }
.bet-timeline-toggle.open { color: var(--blue); }
.bet-timeline-caret { font-size: 10px; color: var(--text-3); }
.bet-timeline-toggle.open .bet-timeline-caret { color: var(--blue); }

.bet-timeline-row td { padding: 0; background: var(--surface-2); }
.bet-timeline-panel { padding: 14px 20px 16px; }
.bet-timeline-msg {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text-3);
}

.bet-timeline-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  margin-bottom: 8px;
  font-size: 11px;
  color: var(--text-2);
}
.bet-timeline-key { display: inline-flex; align-items: center; gap: 6px; }
.bet-timeline-swatch {
  width: 12px;
  height: 3px;
  border-radius: 2px;
  display: inline-block;
}
/* Fair (no-vig) reference — a dotted line segment, mirroring the muted dotted
   overlay in the chart (never a solid semantic swatch). */
.bet-timeline-swatch-fair {
  height: 0;
  border-radius: 0;
  border-top: 2px dotted var(--text-3);
}
.bet-timeline-now { font-family: var(--font-mono); color: var(--text); }
.bet-timeline-axis-note {
  margin-top: 4px;
  font-size: 10px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ------------------------------------------------------------------ */
/* Weekly edge volume (#40) — a section of the Gate view since #258.
   The criterion-card rules that lived here went with the duplicate criteria
   2 & 3 cards; Gate's own .gate-criterion-* rules are the single set now. */
/* ------------------------------------------------------------------ */
.edge-log-msg {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-3);
  padding: 24px 0;
}

/* 12px, not the 6px this carried as a tab header: the status line's neighbour
   below is now the weeks table itself (or the status reason), not a section
   header with its own leading. */
.edge-log-status-line {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-bottom: 12px;
}
.edge-log-status-pill {
  display: inline-block;
  padding: 3px 10px;
  border-radius: var(--radius-sm);
  font-size: 11px;
  font-weight: 700;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.edge-log-status-ok    { background: var(--green-dim); color: var(--green); }
.edge-log-status-muted { background: var(--surface-3); color: var(--text-3); }

.edge-log-status-metric {
  font-size: 12px;
  font-family: var(--font-mono);
  color: var(--text-2);
}

.edge-log-status-reason {
  font-size: 12px;
  color: var(--text-3);
  max-width: 640px;
  line-height: 1.5;
  margin-bottom: 20px;
}

.edge-log-week-no-data td { color: var(--text-3); }

.edge-log-label {
  display: inline-block;
  padding: 3px 10px;
  border-radius: var(--radius-sm);
  font-size: 11px;
  font-weight: 700;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.edge-log-label-pass               { background: var(--green-dim); color: var(--green); }
.edge-log-label-marginal           { background: var(--amber-dim); color: var(--amber); }
.edge-log-label-fail               { background: var(--red-dim);   color: var(--red);   }
.edge-log-label-insufficient_data  { background: var(--surface-3); color: var(--text-3); }
.edge-log-label-unknown            { background: var(--surface-3); color: var(--text-3); }

/* ------------------------------------------------------------------ */
/* Health tab (#45 slice 2) — GET /api/ledger/health report            */
/* ------------------------------------------------------------------ */
.ledger-health-msg {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-3);
  padding: 24px 0;
}

.ledger-health-headline {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 18px 20px;
}
.ledger-health-headline-icon {
  font-size: 22px;
  line-height: 1;
  flex-shrink: 0;
}
.ledger-health-headline-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 4px;
}
.ledger-health-headline-sub {
  font-size: 12px;
  color: var(--text-3);
}
.ledger-health-headline-ok  .ledger-health-headline-icon { color: var(--green); }
.ledger-health-headline-bad .ledger-health-headline-icon { color: var(--red); }
.ledger-health-headline-bad { border-color: var(--red-border); }

.ledger-health-checks-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
  gap: 16px;
  margin-bottom: 8px;
}

.ledger-health-check-card { margin-bottom: 0; }
.ledger-health-check-card .card-header { justify-content: space-between; }
.ledger-health-check-fail { border-color: var(--red-border); }

.ledger-health-check-count {
  font-family: var(--font-mono);
  font-size: 12px;
  font-weight: 700;
  padding: 2px 9px;
  border-radius: var(--radius-sm);
}
.ledger-health-check-count-ok  { background: var(--surface-3); color: var(--text-3); }
.ledger-health-check-count-bad { background: var(--red-dim);   color: var(--red);    }

.ledger-health-check-body { padding: 14px 20px 16px; }
.ledger-health-check-description {
  font-size: 12px;
  color: var(--text-3);
  line-height: 1.5;
  margin-bottom: 10px;
}

.ledger-health-bet-ids summary,
.ledger-health-bet-ids-more summary {
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  color: var(--text-2);
  user-select: none;
}
.ledger-health-bet-ids summary:hover,
.ledger-health-bet-ids-more summary:hover { color: var(--text); }

.ledger-health-bet-id-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  max-height: 220px;
  overflow-y: auto;
}
.ledger-health-bet-id {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-2);
  padding: 4px 0;
  border-bottom: 1px solid var(--border);
  overflow-wrap: anywhere;
  word-break: break-all;
}
.ledger-health-bet-id:last-child { border-bottom: none; }

.ledger-health-bet-ids-more {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

/* ------------------------------------------------------------------ */
/* Performance view (#257) — the merged CLV / Trends / Analytics scorecard */
/* ------------------------------------------------------------------ */
.perf-msg {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-3);
  padding: 24px 0;
}

/* Null roi / hit_rate / beat-close rate: a muted dash, distinct from a genuine
   0 — same text-3 token as .clv-na (mean_clv's own null render via fmtClv). */
.perf-metric-na { color: var(--text-3); }

.perf-overall-row td {
  font-weight: 700;
  background: var(--surface-2);
}

/* ------------------------------------------------------------------ */
/* Gate tab (#48) — GET /api/gate decision-gate scorecard              */
/* ------------------------------------------------------------------ */
.gate-msg {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-3);
  padding: 24px 0;
}

/* One line under a section header saying why the section is on this view.
   Used by the weekly edge-volume section #258 folded in from the retired Edge
   Log tab, which has to read as the evidence behind criteria 2 & 3 above it
   rather than as a second, unrelated scorecard. */
.gate-section-note {
  font-size: 12px;
  color: var(--text-3);
  line-height: 1.5;
  max-width: 640px;
  margin-top: -4px;
  margin-bottom: 12px;
}

.gate-headline {
  padding: 20px 24px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.gate-headline-top {
  display: flex;
  align-items: center;
  gap: 14px;
}
.gate-headline-icon {
  font-size: 20px;
  line-height: 1;
  flex-shrink: 0;
}
.gate-headline-title {
  font-size: 18px;
  font-weight: 700;
  color: var(--text);
}
.gate-headline-rationale {
  font-size: 13px;
  color: var(--text-2);
  line-height: 1.6;
  max-width: 720px;
}

.gate-headline-keep         { border-color: var(--green-border); }
.gate-headline-keep         .gate-headline-icon { color: var(--green); }
.gate-headline-buy          { border-color: var(--amber); }
.gate-headline-buy          .gate-headline-icon { color: var(--amber); }
.gate-headline-insufficient { border-color: var(--blue-border); }
.gate-headline-insufficient .gate-headline-icon { color: var(--blue); }
.gate-headline-unknown      .gate-headline-icon { color: var(--text-3); }

.gate-buy-caveat {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 10px 14px;
  border-radius: var(--radius);
  background: var(--amber-dim);
  border: 1px solid var(--amber);
  font-size: 13px;
  color: var(--text);
  line-height: 1.5;
}
.gate-buy-caveat-label {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--amber);
  flex-shrink: 0;
}

.gate-trigger-pill {
  display: inline-block;
  padding: 3px 10px;
  border-radius: var(--radius-sm);
  font-size: 11px;
  font-weight: 700;
  font-family: var(--font-mono);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.gate-trigger-pill-met     { background: var(--green-dim);  color: var(--green); }
.gate-trigger-pill-not-met { background: var(--surface-3);  color: var(--text-3); }

.gate-trigger-card { margin-bottom: 0; }
.gate-trigger-body { padding: 14px 20px 16px; }

.gate-stat {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 0;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
}
.gate-stat:last-of-type { border-bottom: none; }
.gate-stat-label { color: var(--text-3); }
.gate-stat-value { color: var(--text); }

.gate-criteria-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 16px;
  margin-bottom: 8px;
}

.gate-criterion-card { margin-bottom: 0; }
.gate-criterion-card .card-header { justify-content: space-between; }
.gate-criterion-body { padding: 14px 20px 16px; }

.gate-criterion-reason {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
  font-size: 12px;
  color: var(--text-3);
  line-height: 1.5;
}

.gate-criterion-sublist {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
.gate-criterion-sublist-label {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-3);
  margin-bottom: 6px;
}
.gate-criterion-sublist ul {
  list-style: none;
  font-size: 12px;
  color: var(--text-2);
  line-height: 1.7;
}
.gate-coverage-flag {
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--amber);
}

/* ------------------------------------------------------------------ */
/* Usage tab (issue 139) — API quota headroom + burn                    */
/* ------------------------------------------------------------------ */
/* The drill-down behind the gate's cost criteria, and the gauge the #209
   budget guardrail reports through. Same quiet register as the System Status
   card: these are facts about an account, and only a genuinely actionable one
   (burn over cap, the brake throttling) takes a warn tone. Tokens only — no
   raw colors. */
.usage-head { margin-bottom: 16px; }

.usage-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
}

.usage-sub {
  margin-top: 4px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-3);
  max-width: 70ch;
}

.usage-meta {
  margin-top: 8px;
  font-size: 11px;
  font-family: var(--font-mono);
  color: var(--text-3);
}

/* The loud half of a stale report, in words — a 10px chip alone is easy to
   miss above a page of confident-looking numbers. */
.usage-warning {
  margin-top: 8px;
  padding: 8px 10px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--amber);
  background: var(--amber-dim);
  border: 1px solid var(--amber);
  border-radius: var(--radius-sm);
}

.usage-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 10px;
  margin-bottom: 16px;
}

.usage-stat {
  display: flex;
  flex-direction: column;
  gap: 5px;
  padding: 12px 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.usage-stat-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-3);
}

.usage-stat-value {
  font-size: 20px;
  font-weight: 600;
  color: var(--text);
}

/* The denominator half of a value ("/ 500", "of 500 used") — present but
   subordinate to the number it qualifies. */
.usage-stat-unit {
  font-size: 12px;
  font-weight: 400;
  color: var(--text-3);
}

.usage-stat-sub {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-3);
}

.usage-stat-note { color: var(--text-3); }

.usage-pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  font-size: 11px;
  font-weight: 600;
  white-space: nowrap;
}
.usage-pill-ok   { background: var(--green-dim); color: var(--green); }
.usage-pill-warn { background: var(--amber-dim); color: var(--amber); }
.usage-pill-bad  { background: var(--red-dim);   color: var(--red);   }
.usage-pill-idle { background: var(--surface-3); color: var(--text-3); }

.usage-card { margin-bottom: 12px; }
.usage-card-body { padding: 12px 16px 14px; }

.usage-bars {
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.usage-bar-row {
  display: grid;
  grid-template-columns: minmax(88px, auto) 1fr minmax(56px, auto);
  align-items: center;
  gap: 10px;
}

.usage-bar-key {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.usage-bar-track {
  display: block;
  height: 8px;
  background: var(--surface-3);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

/* A bar is drawn only for a period with a KNOWN cost. An unknown period gets
   an empty track, which reads as "not measured" — never a zero-width bar
   sitting on the baseline as though nothing was spent. */
.usage-bar-fill {
  display: block;
  height: 100%;
  background: var(--blue);
  border-radius: var(--radius-sm);
}

.usage-bar-value {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text);
  text-align: right;
}
.usage-bar-value-unknown { color: var(--text-3); font-style: italic; }

.usage-empty {
  font-size: 12px;
  color: var(--text-3);
}

/* ------------------------------------------------------------------ */
/* Responsive — phone layout (issue 163)                                */
/* ------------------------------------------------------------------ */
/* The header's brand + 6 action buttons + nav need more width than a phone
   has, so below this breakpoint each part takes a row of its own:
     - row 1: brand + actions, the buttons wrapping onto as many lines as they
       need rather than pushing the document into horizontal scroll;
     - row 2: the three section chips (#253), which fit a phone unscrolled;
     - row 3: the active section's sub-nav, scrolling horizontally with a
       right-edge fade as the affordance and trailing space so the last view
       clears it (issue 163's approved approach 1 — a CSS-only scroll strip,
       not collapse-to-menu, so every view stays one tap away).
   The two-level nav is what makes row 3 short: a section holds at most six
   views, so the strip usually fits and the scroll is the exception rather than
   the only way to reach half the app.
   Above the breakpoint nothing here applies and the desktop layout is
   unchanged. Token-only block: every color must stay a var(--...) so
   scripts/design_audit.py stays clean. */
@media (max-width: 640px) {
  .header {
    flex-wrap: wrap;
    height: auto;
    padding: 8px 16px;
    gap: 8px 12px;
  }

  /* Row 1: brand + actions. The action buttons wrap onto as many lines as
     they need rather than overflowing the viewport, so every one stays
     tappable without a horizontal page scroll. */
  .logo { order: 0; }
  .header-actions {
    order: 1;
    /* basis 0, not auto: with an auto basis the block's ~500px of buttons is
       its hypothetical size, so it wraps to a line of its own and strands the
       logo alone on the row above. At 0 it always shares row 1 with the logo,
       then grows into the space left and wraps its buttons internally. */
    flex: 1 1 0;
    min-width: 0;
    flex-wrap: wrap;
    justify-content: flex-end;
  }

  /* Row 2: the section chips, full width and never scrolling — three chips fit
     the narrowest phone, so every section is always one tap away. */
  .nav-groups {
    order: 2;
    flex: 0 0 100%;
    min-height: 0;
  }

  /* Row 3: the sub-nav strip, full width. .nav-scroll is the non-scrolling
     positioning context for the fade; .nav inside it does the scrolling. The
     negative margins match this block's 16px header padding, not the desktop
     24px, so the divider still runs edge to edge. */
  .nav-scroll {
    order: 3;
    flex-basis: calc(100% + 32px);
    margin: 0 -16px;
    padding: 0 16px;
  }
  .nav {
    overflow-x: auto;
    overflow-y: hidden;
    flex-wrap: nowrap;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: thin;
    scrollbar-color: var(--border-focus) transparent;
    /* Trailing space so the last tab can be scrolled clear of the fade. */
    padding-right: 28px;
  }
  .nav-btn {
    flex: 0 0 auto;
    white-space: nowrap;
    scroll-snap-align: start;
  }
  /* Right-edge fade: signals "there is more to the right". Decorative and
     non-interactive, so it never swallows a tap on the tab beneath it. */
  .nav-scroll::after {
    content: "";
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    width: 28px;
    background: linear-gradient(to right, transparent, var(--surface));
    pointer-events: none;
  }

  /* The five stat cards are `flex: 1; min-width: 120px`, so the bar demands
     ~604px and is the one thing left forcing a horizontal scroll on the whole
     document. That matters to the nav: .header is sticky vertically only, so
     a horizontally scrolled page drags the tab strip sideways and parks the
     leading tabs off-screen with no way back except scrolling the page. Let
     the cards wrap onto as many rows as they need instead. */
  .stats-bar { flex-wrap: wrap; }

  /* The skip-to-content jump has to clear the header, which is sticky and now
     several rows tall: measured 100px at 640px, 136px at 360-480px, and 172px
     at 320px where the action buttons need a third row, plus the ~52px section-
     chip row the two-level nav adds (#253). Sized for that worst case — the
     desktop value would leave the top of the content hidden under the header.
     Overshooting on a shorter header only reveals a little of the stats bar
     above the content, which is harmless. */
  .content { scroll-margin-top: 232px; }
}

/* ------------------------------------------------------------------ */
/* Mobile entry path — Log Bet + Settle modals, form controls          */
/*                                                                      */
/* The other half of the mobile-nav breakpoint above: that one fixed   */
/* the nav, this one fixes data entry. Desktop (>=481px) is untouched. */
/* Token-only block: every color must stay a var(--...) so             */
/* scripts/design_audit.py stays clean.                                */
/* ------------------------------------------------------------------ */
@media (max-width: 480px) {
  /* iOS Safari force-zooms the page on focus of any input under 16px;
     scoping the bump to mobile keeps desktop's 13px untouched. The Bets
     filter bar's date-range fields (#254) are inputs like any other and were
     simply missed when they landed — at 13px they force-zoom the whole page
     on tap, which is the exact bug this rule exists to prevent. */
  input[type="text"],
  input[type="number"],
  input[type="datetime-local"],
  input[type="date"],
  select {
    font-size: 16px;
  }

  /* 44px minimum touch target on every control in the entry path.
     The Bets filter bar (#254) and the Market filter bar (#255) both landed
     after this block was written, so their controls were never brought under
     it: the result chips, the two date-range fields, and either bar's Clear
     button all sit around 26-31px. Those are the settle workflow's controls
     on a phone — "Open" is one tap from the bets that need settling — so they
     belong here next to .mf-chip, which was the one that did get added.
     The sortable column headers (.bets-sort-btn) are deliberately left out:
     they anchor a <th>, so a 44px floor would grow every sortable table's
     header row rather than just the control. Worth its own look. */
  input[type="text"],
  input[type="number"],
  input[type="datetime-local"],
  input[type="date"],
  select,
  .form-actions .btn,
  .settle-btn,
  .edit-btn,
  .track-btn,
  .nav-group-btn,
  .nav-btn,
  .mf-chip,
  .bets-filter-chip,
  .bets-filter-bar .btn,
  .market-filter-bar .btn,
  .pick-book,
  .card-header-toggle {
    min-height: 44px;
  }
  .modal-close {
    padding: 10px 14px;
  }

  /* Paired fields stack to one column instead of the ~150px-wide pair. */
  .form-row {
    grid-template-columns: 1fr;
  }

  /* Full-width, top-anchored modal with tighter horizontal padding. */
  .modal {
    max-width: none;
    max-height: 100vh;
    border-radius: 0;
  }
  .modal-header {
    padding: 16px 16px 14px;
  }
  .bet-form {
    padding: 16px 16px 20px;
  }
  .settle-bet-info {
    padding: 12px 16px;
  }

  /* Save/Settle stays reachable on a short viewport: .modal is the scroll
     container and .bet-form has no intervening overflow, so a sticky
     footer resolves against .modal correctly. The opaque background keeps
     scrolled content from showing through underneath it. */
  .form-actions {
    position: sticky;
    bottom: 0;
    background: var(--surface);
  }

  /* Market filter bar (#255): search takes its own row and the two selects
     split the next one, rather than cramming three controls onto a 375px
     line. Chips already wrap. */
  .mf-search { flex: 1 1 100%; }
  .mf-select { flex: 1 1 calc(50% - 4px); }

  /* Game picker (#415): tighter padding, since its own box sits inside the
     form's 16px gutters. Its Sport/Game pair stacks with every other
     .form-row above. The book buttons grow to the 44px floor and each takes a
     full line, so the price sits well clear of the book name at 375px. */
  .bet-picker { padding: 10px 10px 12px; }
  .pick-book { flex: 1 1 100%; }
  .pick-book-name { margin-right: auto; }
}

/* ------------------------------------------------------------------ */
/* Form login (issue 212)                                                   */
/* ------------------------------------------------------------------ */
/* The login view replaces the entire dashboard when a session is required
   and absent. Phone-usable per issue 205: 16px inputs (so iOS Safari does not
   zoom on focus) and >=44px touch targets. Tokens only. */
.login-wrap {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
}
.login-card {
  width: 100%;
  max-width: 360px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 28px 24px;
  display: flex;
  flex-direction: column;
}
.login-brand {
  font-size: 20px;
  font-weight: 700;
  color: var(--text);
  letter-spacing: 0.5px;
}
.login-sub {
  font-size: 13px;
  color: var(--text-2);
  margin: 4px 0 18px;
}
.login-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-2);
  margin-bottom: 6px;
}
.login-input {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  color: var(--text);
  font: inherit;
  font-size: 16px;
  padding: 11px 12px;
  width: 100%;
  min-height: 44px;
  outline: none;
  margin-bottom: 14px;
  transition: border-color 0.15s;
  -webkit-appearance: none;
}
.login-input:focus { border-color: var(--blue); }
.login-input:focus-visible {
  outline: 2px solid var(--blue);
  outline-offset: 2px;
}
.login-input::placeholder { color: var(--text-3); }
.login-submit {
  width: 100%;
  min-height: 44px;
  font-size: 14px;
  margin-top: 4px;
  justify-content: center;
}
/* Change password panel (issue 251). Reuses .login-label / .login-input /
   .login-error so the app's two password forms are visibly one control set; the
   only new rules are a readable measure (a config section is far wider than the
   login card, and full-width password fields on a desktop look broken) and a
   submit button sized to its label rather than to the card. */
.password-form {
  display: flex;
  flex-direction: column;
  max-width: 360px;
  padding-top: 8px;
}
.password-form .login-submit {
  width: auto;
  align-self: flex-start;
}

.login-error {
  font-size: 13px;
  color: var(--red);
  background: var(--red-dim);
  border: 1px solid var(--red-border);
  border-radius: var(--radius-sm);
  padding: 8px 10px;
  margin-bottom: 14px;
}

/* ------------------------------------------------------------------ */
/* Movement tab (issue 143) — pre-match open→latest line movement       */
/* ------------------------------------------------------------------ */
/* The ranked cross-selection report behind Lines' per-selection sparkline.
   Same quiet register as the price tabs: the direction chips carry the only
   color, because the direction is the whole message. Tokens only — no raw
   colors. */
.move-head { margin-bottom: 16px; }

.move-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
}

.move-sub {
  margin-top: 4px;
  margin-bottom: 10px;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-3);
  max-width: 70ch;
}

/* Two lines in one cell: the prices above, the signed delta and its chip
   below — so a wide "open → latest, Δ, direction" column stays narrow enough
   for a phone without dropping either half. */
.move-cell { white-space: nowrap; }

.move-prices {
  display: block;
  font-family: var(--font-mono);
  color: var(--text);
}

.move-delta {
  display: block;
  margin-top: 2px;
  font-size: 11px;
  color: var(--text-3);
}

.move-chip {
  display: inline-block;
  margin-left: 4px;
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.04em;
  white-space: nowrap;
}
.move-chip-pos  { background: var(--green-dim); color: var(--green); }
.move-chip-neg  { background: var(--red-dim);   color: var(--red);   }
.move-chip-flat { background: var(--surface-3); color: var(--text-3); }

/* What the colors mean, in words, under the table — a green chip in the
   benchmark column is a CLV reading and a green chip in your own column is
   just a better price, and nothing about the two colors says so on its own. */
.move-legend {
  margin-top: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text-3);
  font-size: 12px;
  line-height: 1.6;
  max-width: 80ch;
}

/* ------------------------------------------------------------------ */
/* fantasy — "did I beat my bench?" (F4)                                */
/* ------------------------------------------------------------------ */
/* The trend: one bar per graded week, sized against the season's worst
   week, so the shape of the season is readable at a glance without a
   charting library. Colors are tokens only, like every other component
   here — scripts/design_audit.py stays at 0 violations. */
.bench-bar {
  height: 8px;
  min-width: 80px;
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  overflow: hidden;
}

.bench-bar-fill {
  height: 100%;
  border-radius: var(--radius-sm);
  background: var(--amber);
}

/* A week where the lineup started WAS the best available. The bar is empty
   by definition, so the row says it in words rather than showing nothing. */
.bench-perfect { color: var(--green); }

/* A week that could not be graded. Muted rather than alarming: a blank is an
   honest answer about the data, not a failure of the team. */
.bench-blank {
  color: var(--text-3);
  font-style: italic;
}

/* Per-week lineup detail, collapsed by default (native <details>, no state
   to lose on a re-render). The projections panel (F6b) folds its by-week error
   table the same way, so it shares the rule rather than restating it. */
.bench-week,
.proj-fold {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  margin-bottom: 6px;
  padding: 8px 12px;
}

.bench-week summary,
.proj-fold summary {
  cursor: pointer;
  color: var(--text-2);
  font-size: 13px;
}

.bench-week summary:hover,
.proj-fold summary:hover { color: var(--text); }

/* A player the model could not project (F6b). Muted rather than alarming, and
   for the same reason a blank bench week is: "we don't know" is an honest
   answer about the inputs, not a fault in the player. */
.proj-blank {
  color: var(--text-3);
  font-style: italic;
}

/* The error carried beside every projected number. Small and always present:
   F6's acceptance is that no projection surface shows a point estimate on its
   own, so this rides in the same cell rather than in a footnote. */
.proj-error {
  color: var(--text-3);
  font-size: 12px;
  white-space: nowrap;
}

/* The model's MAE minus a benchmark's. Negative is the model closer to the
   truth, so green; positive is a loss to that benchmark, and F6's acceptance is
   explicit that a loss is a finding to report, not a thing to soften. Coloured
   only when the comparison was actually measured — an unmeasured one gets the
   muted treatment instead, never a colour that implies a result. */
.proj-better { color: var(--green); }
.proj-worse  { color: var(--red); }

.bench-slot-line {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 6px 0;
  border-top: 1px solid var(--border);
  font-size: 12px;
}

.bench-slot-name {
  min-width: 88px;
  color: var(--text-3);
  font-weight: 600;
  letter-spacing: 0.04em;
}

/* ------------------------------------------------------------------ */
/* Typed draft picks (F8c, #392) — the entry form on /fantasy          */
/* ------------------------------------------------------------------ */
/* Its own layout rather than a reuse of .book-add-form: that form is all
   free-text inputs, and its `input { flex: 1; min-width: 100px }` would
   stretch this one's checkbox into a 100px-wide box. Same shape, same
   spacing, no colours of its own — every colour on this card comes from the
   card/badge/button components it is built out of. Laid out to wrap, because
   this is typed one-handed on a phone during an auction. */
.manual-pick-form {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  padding: 8px 0 0;
}
.manual-pick-form input[type="text"] {
  flex: 1;
  min-width: 110px;
  width: auto;
}
.manual-pick-form select {
  flex: 0 1 auto;
  width: auto;
}
.manual-pick-form .btn { flex-shrink: 0; }

.manual-pick-check {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
.manual-pick-check input {
  width: auto;
  min-width: 0;
  flex: none;
}
