/* =========================================================================
   NFL Prediction Engine V2 — Convexity color palette + dark theme
   ========================================================================= */

:root {
  --convexity-ev-high: #4CAF50;
  --convexity-ev-mid: #FFD700;
  --convexity-ev-model: #3b9eff;
  --convexity-ev-consensus-only: #FFB020;
  --convexity-ev-low: #FF8C00;
  --convexity-ev-negative: #F44336;
  /* Deliberately distinct from the EV-outline colors above (gold/blue/
     green all mean something specific already) - "in your parlay" is an
     unrelated signal and reusing one of those would be confusing. */
  --convexity-parlay: #a970ff;

  --convexity-bg: #121212;
  --convexity-card-bg: #1E1E1E;
  --convexity-text: #E0E0E0;
  --convexity-border: #333;

  /* Matches Bootstrap 5.3's dark-theme --bs-secondary-color (the color
     behind .text-muted, used everywhere for a non-positive Consensus EV
     value) - defined here as a real paintable color so the Model EV "zero"
     box (see .model-ev-box-zero) can reuse the exact same off-white as a
     background instead of a text-only utility class. */
  --convexity-off-white: rgba(255, 255, 255, 0.75);
  /* Bootstrap's own neutral grey (--bs-secondary / .text-bg-secondary) -
     used only for the Total column's "standard -110 price" shading box. */
  --convexity-neutral-grey: #6c757d;
}

html, body {
  background-color: var(--convexity-bg);
  color: var(--convexity-text);
  min-height: 100%;
}

body {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

main {
  flex: 1 0 auto;
  min-width: 0;
  width: 100%;
}

#game-detail-content {
  min-width: 0;
  max-width: 100%;
}

a {
  color: #8ab4ff;
}

a:hover {
  color: #b6cdff;
}

/* -------------------------------------------------------------------------
   EV badges
   ------------------------------------------------------------------------- */

.ev-badge {
  display: inline-block;
  padding: 0.3em 0.65em;
  font-size: 0.85em;
  font-weight: 700;
  border-radius: 0.375rem;
  white-space: nowrap;
}

.ev-high { background-color: var(--convexity-ev-high); color: #000; }
.ev-mid { background-color: var(--convexity-ev-mid); color: #000; }
.ev-low { background-color: var(--convexity-ev-low); color: #000; }
.ev-negative { background-color: var(--convexity-ev-negative); color: #fff; }

/* -------------------------------------------------------------------------
   Cards / tables
   ------------------------------------------------------------------------- */

.card-dark {
  background-color: var(--convexity-card-bg);
  border: 1px solid var(--convexity-border);
  color: var(--convexity-text);
}

.card-dark .card-header {
  background-color: #262626;
  border-bottom: 1px solid var(--convexity-border);
  color: var(--convexity-text);
  font-weight: 600;
}

.table-dark-custom {
  /* Bootstrap applies --bs-table-bg per-cell (table > :not(caption) > * > *),
     so overriding the variable itself is required. */
  --bs-table-bg: var(--convexity-bg);
  --bs-table-color: var(--convexity-text);
  --bs-table-border-color: var(--convexity-border);
  --bs-table-striped-bg: #1a1a1a;
  --bs-table-striped-color: var(--convexity-text);
  --bs-table-hover-bg: #202020;
  --bs-table-hover-color: var(--convexity-text);
  background-color: var(--convexity-bg);
  color: var(--convexity-text);
  border-color: var(--convexity-border);
}

.table-dark-custom th,
.table-dark-custom td {
  border-color: var(--convexity-border);
  vertical-align: middle;
}

.table-dark-custom thead th {
  background-color: #1a1a1a;
  color: #b9b9b9;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  position: sticky;
  top: 0;
  z-index: 1;
}

/* Sortable table headers */
.sortable {
  cursor: pointer;
  user-select: none;
  position: relative;
  white-space: nowrap;
}

.sortable:hover {
  color: #fff;
}

.sortable .sort-indicator {
  margin-left: 0.35em;
  opacity: 0.5;
  font-size: 0.75em;
}

.sortable.sort-active .sort-indicator {
  opacity: 1;
}

/* -------------------------------------------------------------------------
   Navbar / footer
   ------------------------------------------------------------------------- */

.navbar-convexity {
  background-color: #0a0a0a;
  border-bottom: 1px solid var(--convexity-border);
}

.navbar-convexity .navbar-brand {
  font-weight: 700;
  letter-spacing: 0.02em;
}

.navbar-logo {
  height: 2.1rem;
  width: auto;
  display: block;
}

/* Bootstrap's navbar container defaults to justify-content: space-between,
   which pushes the whole nav-links collapse flush against the right edge
   of the viewport regardless of margin utilities on the <ul> inside it -
   at narrow-ish desktop widths that left the Tools dropdown's menu with
   nowhere to open but off the right edge of the screen. flex-start plus an
   explicit gap keeps the nav links close after the brand instead. */
.navbar-convexity .container-fluid {
  position: relative;
  justify-content: flex-start;
  gap: 1.5rem;
}

/* Empty on every page except games.html (see games.js) - anchored to the
   navbar's right edge, not page-center. It used to sit at left: 50%, but
   the brand+nav-links are now flex-start (packed toward the left, see
   .navbar-convexity .container-fluid above) - on a narrower/portrait
   viewport the nav links reach past the true horizontal center and
   collide with page-centered text. Right-anchoring keeps it in the space
   that flex-start already frees up on the right, instead of fighting the
   nav links for the same spot. */
.navbar-center-title {
  position: absolute;
  right: 1rem;
  top: 50%;
  transform: translateY(-50%);
  font-weight: 700;
  letter-spacing: 0.02em;
  font-size: 1.1rem;
  color: #cfcfcf;
  white-space: nowrap;
  pointer-events: none;
}

.navbar-convexity .nav-link {
  color: #cfcfcf;
}

.navbar-convexity .nav-link.active,
.navbar-convexity .nav-link:hover {
  color: #fff;
}

.us-ref-books-toggle {
  color: #cfcfcf;
  font-size: 0.85rem;
  white-space: nowrap;
  padding-top: 0.35rem;
  user-select: none;
}

.us-ref-books-toggle .form-check-input {
  cursor: pointer;
  margin-top: 0.2rem;
}

.us-ref-books-toggle .form-check-input:checked {
  background-color: var(--convexity-ev-mid);
  border-color: var(--convexity-ev-mid);
}

.us-ref-books-toggle .form-check-label {
  cursor: pointer;
  color: #cfcfcf;
}

/* Sits next to the brand text rather than in the right-aligned nav list,
   so it stays put (and out of the way of Games/Book View/Help/Log out)
   whether the navbar is expanded or collapsed behind the hamburger. */
.navbar-brand-group {
  flex-wrap: wrap;
}

.book-filter-mode-select {
  max-width: 12rem;
  font-size: 0.85rem;
}

.site-footer {
  background-color: #0a0a0a;
  border-top: 1px solid var(--convexity-border);
  color: #888;
  font-size: 0.85rem;
  padding: 0.5rem 0;
  margin-top: 1rem;
}

.footer-legal-links {
  margin-top: 0.2rem;
  font-size: 0.8rem;
}

.footer-legal-links a {
  color: #888;
}

.footer-legal-links a:hover {
  color: #b6cdff;
}

.footer-legal-sep {
  margin: 0 0.4em;
}

/* -------------------------------------------------------------------------
   Misc
   ------------------------------------------------------------------------- */

.page-title {
  margin: 1.5rem 0 1rem;
}

/* games.html - lets the "Loading..." indicator center on the page,
   independent of the "NFL Games" heading's own width, same centering
   technique as .book-name-heading/.navbar-center-title. */
.page-title-row {
  position: relative;
  margin-bottom: 1rem;
}

.games-loading-heading {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  color: #9c9c9c;
  font-weight: 600;
  white-space: nowrap;
}

.empty-state,
.error-state {
  color: #999;
  padding: 2rem;
  text-align: center;
}

.error-state {
  color: var(--convexity-ev-negative);
}

.status-banner {
  display: inline-block;
  padding: 0.35rem 0.75rem;
  border-radius: 0.4rem;
  font-size: 0.82rem;
  font-weight: 600;
  margin-bottom: 0.75rem;
}

.status-banner.usage-ok {
  background: #262626;
  color: #b9b9b9;
  border: 1px solid var(--convexity-border);
}

.status-banner.usage-warn {
  background: #4a3200;
  color: #ffd27a;
  border: 1px solid #7a5300;
}

.status-banner.usage-danger {
  background: #4a1414;
  color: #ff9c9c;
  border: 1px solid #7a1f1f;
}

.model-fair-cell {
  color: #9ad4ff;
  font-weight: 600;
  white-space: nowrap;
}

/* book.html/ncaa-book.html - every column header centered, regardless of
   whether that column's own body cells are centered (Spread/Moneyline/
   Model Away/Model Home - see book.js/ncaa-book.js) or left-justified
   (Kickoff, Matchup, Total - wide enough that left works fine there). */
#book-table-wrap thead th {
  text-align: center;
}

/* Uniform proposition-box width for the Spread/Run Line/Puck Line and
   Moneyline Away/Home columns (Paula, 2026-09-09: keep all four outline
   boxes the same size, since a spread cell's "+3.5 -110" is naturally
   wider than a moneyline cell's "-150" otherwise). These four always land
   in columns 4-7 of #book-table-wrap's <thead> regardless of which pair
   a given sport shows first - Spread then Moneyline for NFL/NBA/NCAA
   football/NCAA basketball, Moneyline then Run Line/Puck Line for
   MLB/NHL (see each book.html's own <thead>) - so one rule covers every
   sport. white-space:nowrap keeps a line+price pair ("+3.5 -110") from
   wrapping once the column is forced wider/narrower than its own content
   would otherwise need. */
#book-table-wrap th:nth-child(4),
#book-table-wrap th:nth-child(5),
#book-table-wrap th:nth-child(6),
#book-table-wrap th:nth-child(7),
#book-table-wrap td:nth-child(4),
#book-table-wrap td:nth-child(5),
#book-table-wrap td:nth-child(6),
#book-table-wrap td:nth-child(7) {
  width: 9rem;
  white-space: nowrap;
}

/* Survivor Pool's Pick/Opponent columns - green for the recommended pick
   matches this app's existing "good" color everywhere else
   (--convexity-ev-high); a light red for the opponent reads as "not this
   one" consistent with red already meaning negative/avoid elsewhere,
   rather than reusing blue, which already has its own meaning (model).
   !important because Bootstrap's table component sets cell text color via
   its own --bs-table-color variable directly on each td - same reason the
   opportunity row colors below need it too. */
.survivor-pick {
  color: var(--convexity-ev-high) !important;
}

.survivor-opponent {
  color: #f4a6a6 !important;
}

/* Compact EV line size - was always Model's size, now applied to whichever
   of Consensus/Model EV renders smaller after the game.html/book.html
   position+size swap (see game.js/book.js). */
.ev-compact {
  font-size: 0.72rem;
  letter-spacing: 0.01em;
}

/* Model EV text color (game.html's per-book cell, plain text, no box):
   negative -> red, positive -> blue (text-info, unchanged from before the
   swap), exactly zero -> the same off-white .text-muted already used for a
   non-positive Consensus value. */
.text-ev-red {
  color: var(--convexity-ev-negative);
}

/* Model EV box (book.html's per-book cell, filled pill - replaces the old
   Consensus ev-badge in that visual role after the swap). Same 3-way rule
   as .text-ev-red above, but as a filled background since book.html shows
   Model EV as a box rather than plain text. */
.model-ev-box {
  display: inline-block;
  padding: 0.3em 0.65em;
  font-size: 0.85em;
  font-weight: 700;
  border-radius: 0.375rem;
  white-space: nowrap;
}

.model-ev-box-negative { background-color: var(--convexity-ev-negative); color: #fff; }
.model-ev-box-positive { background-color: var(--convexity-ev-model); color: #fff; }
.model-ev-box-zero { background-color: var(--convexity-off-white); color: #1a1a1a; }

/* book.html Total column shading - fills the whole total-subcell box based
   on that leg's own raw American price, independent of the existing
   opportunity outline ring (cellOutlineClass/.opportunity-*-cell), which is
   a separate CSS property (outline vs background) and is left unchanged. */
.total-shade-grey { background-color: var(--convexity-neutral-grey); color: #fff; }
.total-shade-blue { background-color: var(--convexity-ev-model); color: #fff; }
.total-shade-red { background-color: var(--convexity-ev-negative); color: #fff; }
.total-shade-green { background-color: var(--convexity-ev-high); color: #000; }

.game-list-row {
  cursor: pointer;
}

.game-list-row:hover td {
  background-color: #202020;
}

/* book.html/ncaa-book.html - brief arrival cue for the row scrolled to via
   scrollToQueryFixtureId (app.js), when arriving from that game's "Book
   View" link on game.html/ncaa-game.html. A fading flash rather than a
   persistent outline so it doesn't collide with the outline colors'
   existing opportunity meanings (gold/blue/green) - this isn't a signal
   about the row's odds, just "you arrived here." JS removes the class
   after the animation finishes. */
@keyframes scroll-highlight-fade {
  0% { background-color: rgba(58, 158, 255, 0.35); }
  100% { background-color: transparent; }
}

.scroll-highlight-target td {
  animation: scroll-highlight-fade 2.5s ease-out;
}

.game-list-row .matchup {
  font-weight: 600;
  color: #f0f0f0;
}

/* Already-kicked-off games stay in the list for the rest of their week
   (see games.js hasKickedOff) but a stale "+EV" claim would be misleading
   once the game's underway, so they get dimmed instead of the opportunity
   colors - still fully clickable through to the game detail page. */
.game-list-row.game-started td {
  opacity: 0.55;
}

.game-list-row.game-started:hover td {
  opacity: 0.8;
}

.game-detail-header {
  margin-bottom: 1rem;
}

.game-detail-header .matchup {
  font-size: 1.4rem;
  font-weight: 700;
}

.game-detail-header .kickoff {
  color: #9c9c9c;
}

/* Wide book grid — horizontal scroll when live odds return many books */
#game-table-wrap {
  overflow-x: auto;
  overflow-y: visible;
  max-width: 100%;
  -webkit-overflow-scrolling: touch;
  margin-bottom: 1rem;
}

#game-table-wrap .table-dark-custom {
  width: max-content;
  min-width: 100%;
  margin-bottom: 0;
}

#game-table-wrap .table-dark-custom th,
#game-table-wrap .table-dark-custom td {
  white-space: nowrap;
}

#game-table-wrap .book-col {
  min-width: 5.25rem;
  text-align: center;
}

/* State book-filter mode: a local/tribal venue's column header is two
   lines (venue name, then "LOCAL" or "LOCAL - <AFFILIATE>") built as a
   single text node with an embedded newline (see rebuildBookColumns) -
   th.textContent gets flattened and rewritten by makeSortable's own
   sort-indicator logic (app.js), so real nested markup here doesn't
   survive; pre-line is what turns that literal "\n" into an actual line
   break. Harmless for every other book-col header, which never contains
   one. */
#game-table-wrap th.book-col {
  white-space: pre-line;
}

/* Stale book column header: subtle dimming, deliberately not a border/
   outline color so it never competes with the opportunity outline colors
   (.opportunity-*-cell below) on the same book's price cells. Hover the
   header for exact last-ingested time. */
#game-table-wrap th.book-col-stale {
  opacity: 0.55;
}

/* State book-filter mode: a tribal/local venue with no submitted price yet
   for this game/market borrows its known affiliate's real current price -
   same "dim, don't recolor" transparency cue as book-col-stale/
   stack-section-stale above (Paula: "I don't like adding words if we can
   avoid it", so no text badge). Kept as its own class, not a reuse of
   book-col-stale, so an estimated price could later also independently be
   stale without stacking two opacity rules. Applies to both the desktop
   grid cell (game.js) and the mobile stacked price block (game.js/book.js). */
.book-price-estimated {
  opacity: 0.55;
}

#game-table-wrap .game-sticky-col {
  position: sticky;
  z-index: 2;
  background-color: var(--convexity-bg);
}

#game-table-wrap thead .game-sticky-col {
  z-index: 4;
  background-color: #1a1a1a;
}

#game-table-wrap .game-sticky-col-0 {
  left: 0;
  min-width: 5.5rem;
}

#game-table-wrap .game-sticky-col-1 {
  left: 5.5rem;
  min-width: 11rem;
  box-shadow: 4px 0 8px -4px rgba(0, 0, 0, 0.45);
}

.market-family-badge {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: #888;
}

.consensus-cell {
  font-weight: 700;
  color: #cbd5e1;
}

.opportunity-both-cell {
  outline: 2px solid var(--convexity-ev-consensus-only);
  outline-offset: -2px;
  border-radius: 0.25rem;
}

.opportunity-model-only-cell {
  outline: 2px solid var(--convexity-ev-model);
  outline-offset: -2px;
  border-radius: 0.25rem;
}

.opportunity-consensus-only-cell {
  outline: 2px solid var(--convexity-ev-high);
  outline-offset: -2px;
  border-radius: 0.25rem;
}

.opportunity-legend-both {
  color: var(--convexity-ev-consensus-only);
  font-weight: 600;
}

.opportunity-legend-model-only {
  color: var(--convexity-ev-model);
  font-weight: 600;
}

.opportunity-legend-consensus-only {
  color: var(--convexity-ev-high);
  font-weight: 600;
}

.opportunity-legend-parlay {
  color: var(--convexity-parlay);
  font-weight: 600;
}

.parlay-cell {
  cursor: pointer;
}

.parlay-cell-selected {
  outline: 2px solid var(--convexity-parlay);
  outline-offset: -2px;
  border-radius: 0.25rem;
}

/* Over/Under stacked in one Total cell (book.html) - each row is its own
   independently-clickable parlay target (see totalSubcellHtml in
   book.js), so they need their own padding/spacing rather than relying
   on the parent <td>'s. */
.total-subcell {
  padding: 0.15rem 0.3rem;
  border-radius: 0.25rem;
}

.total-subcell + .total-subcell {
  margin-top: 0.35rem;
}

.parlay-toggle-btn {
  background-color: var(--convexity-card-bg);
  border: 1px solid var(--convexity-parlay);
  color: var(--convexity-parlay);
  font-weight: 600;
}

.parlay-toggle-btn:hover {
  background-color: var(--convexity-parlay);
  color: #1a0f2e;
}

.parlay-toggle-btn .badge {
  background-color: var(--convexity-parlay);
  color: #1a0f2e;
}

.parlay-toggle-btn:hover .badge {
  background-color: #1a0f2e;
  color: var(--convexity-parlay);
}

/* Was a flat 22rem regardless of viewport - fine on a wide landscape
   monitor, but on a narrower/portrait viewport that fixed width can eat
   enough of the screen to visually cover grid columns (e.g. Spread Away)
   out from under the offcanvas, since data-bs-backdrop="false" means the
   grid stays visible/interactive underneath rather than being dimmed.
   clamp() scales the width down on narrower viewports. The "Or add a leg
   manually" row (see book.html) was restructured to stack the description
   input above the odds+Add row instead of all three side by side, which
   is what let the floor drop this far without that row breaking.

   IMPORTANT: this MUST be an ID selector (#parlayOffcanvas), not a class
   (.parlay-offcanvas) - every width value tried here before this comment
   was silently ignored, because Bootstrap sets the actual offcanvas width
   via compound two-class selectors (.offcanvas.offcanvas-end, plus one
   per breakpoint variant), specificity (0,2,0), which beats a single
   class selector like .parlay-offcanvas at (0,1,0) regardless of
   stylesheet order - source order only breaks ties when specificity is
   equal. An ID selector's (1,0,0) beats any number of classes, so this
   is the one property that has to stay on the ID rather than the class. */
#parlayOffcanvas {
  /* Deliberately wide enough to fully hide Model Home/Model Away/Pick
     while keeping Spread Away fully visible on the book.html grid, per
     explicit feedback - not just "as thin as possible" like earlier
     attempts. Calibrated by eye against screenshots, not measured live -
     expect this to need one more round of tuning. */
  width: clamp(18rem, 28vw, 22rem);
}

.parlay-offcanvas {
  background-color: var(--convexity-card-bg);
  color: var(--convexity-text);
  border-left: 1px solid var(--convexity-border);
}

.parlay-offcanvas .btn-close {
  opacity: 0.8;
}

.parlay-leg-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  background-color: var(--convexity-bg);
  border: 1px solid var(--convexity-border);
  border-radius: 0.4rem;
  padding: 0.5rem 0.65rem;
  margin-bottom: 0.5rem;
}

.parlay-leg-item .parlay-leg-remove {
  background: none;
  border: none;
  color: var(--convexity-ev-negative);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  padding: 0 0.25rem;
}

/* legacy aliases */
.consensus-opportunity-cell,
.best-price-cell {
  outline: 2px solid var(--convexity-ev-high);
  outline-offset: -2px;
  border-radius: 0.25rem;
}

.model-opportunity-cell {
  outline: 2px solid var(--convexity-ev-model);
  outline-offset: -2px;
  border-radius: 0.25rem;
}

.opportunities-banner {
  padding: 0.75rem 1rem;
  border-radius: 0.4rem;
  margin-bottom: 1rem;
  font-size: 0.9rem;
}

.opportunities-banner.opportunities-found {
  background: #103a1d;
  border: 1px solid #1f6b39;
}

/* Sized to fit the full "vs consensus" + "vs model" content (each up to 4
   lines, per renderOpportunityList's own cap) plus both headers and the
   legend line - the banner's item count varies with whichever model is
   selected, and without a floor here the whole page reflows underneath it
   every time (glaring with the Cycle button running - see game.js -
   switching models multiple times a second). Desktop-only: mobile's
   already-scarce vertical space isn't worth reserving for the rare short-
   content case, and Cycle isn't the expected mobile use anyway. */
@media (min-width: 576px) {
  .opportunities-banner.opportunities-found {
    min-height: 11rem;
  }
}

.opportunities-banner.opportunities-found ul {
  list-style: none;
}

.opportunities-banner.opportunities-none {
  background: #1a1a1a;
  border: 1px solid var(--convexity-border);
  color: #999;
}

/* -------------------------------------------------------------------------
   Games list — collapsible week sections
   ------------------------------------------------------------------------- */

.week-header .week-actions {
  flex-shrink: 0;
}

.week-header .week-actions .btn {
  white-space: nowrap;
}

.week-block {
  border: 1px solid var(--convexity-border);
  border-radius: 0.4rem;
  margin-bottom: 0.75rem;
  overflow: hidden;
  background: var(--convexity-card-bg);
}

.week-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.65rem 1rem;
  cursor: pointer;
  list-style: none;
  background: #1a1a1a;
  user-select: none;
}

.week-header::-webkit-details-marker {
  display: none;
}

.week-header::before {
  content: "▸";
  display: inline-block;
  margin-right: 0.5rem;
  color: #888;
  transition: transform 0.15s ease;
}

details[open] > .week-header::before {
  transform: rotate(90deg);
}

.week-header .week-label {
  font-weight: 700;
  color: #f0f0f0;
}

.week-header.opportunity-week {
  background: #14532d;
  border-bottom: 1px solid #22c55e;
  box-shadow: inset 4px 0 0 var(--convexity-ev-high);
}

.week-header.opportunity-week .week-label {
  color: #86efac;
}

.week-block:has(> .opportunity-week) {
  border-color: #22c55e;
}

/* A week with a golden (model + consensus) opportunity outranks a plain
   consensus-only one - same gold used for opportunity-row-dual/the grid's
   "both" outline, not the green above. */
.week-header.opportunity-week-dual {
  background: #3a2f14;
  border-bottom: 1px solid #a67c1e;
  box-shadow: inset 4px 0 0 var(--convexity-ev-consensus-only);
}

.week-header.opportunity-week-dual .week-label {
  color: #f0d58a;
}

.week-block:has(> .opportunity-week-dual) {
  border-color: var(--convexity-ev-consensus-only);
}

.week-header .scan-week-btn {
  white-space: nowrap;
}

.week-block table {
  margin-bottom: 0;
}

/* The table stays full width (Bootstrap's .table default) so the gold/
   green opportunity highlight still spans the whole row - but without a
   width hint, the real content columns are what absorb leftover space on
   a wide monitor, stranding the score/freshness column far to the right
   with a big gap before it, or (worse - the actual bug this fixed)
   getting squeezed down to near-nothing and wrapping word-by-word once
   the spacer below claims the leftover space instead. Every real content
   column needs this, not just matchup/status - the kickoff column has no
   class of its own to hang a selector off, hence :not(.game-row-spacer)
   rather than an explicit list. */
.week-block table td:not(.game-row-spacer) {
  white-space: nowrap;
}

.week-block table .game-row-spacer {
  width: 100%;
}

.opportunity-row td {
  background-color: #103a1d !important;
  color: #d8f5e1 !important;
}

.opportunity-row td:first-child {
  box-shadow: inset 4px 0 0 var(--convexity-ev-high);
}

.opportunity-row .matchup {
  color: #7dffa0 !important;
  font-weight: 700;
}

.opportunity-row .opportunity-status {
  color: #9ad4ad !important;
}

.opportunity-row:hover td {
  background-color: #164f28 !important;
}

.opportunity-row-dual td {
  background-color: #3a2f14 !important;
  color: #f0d58a !important;
}

.opportunity-row-dual td:first-child {
  box-shadow: inset 4px 0 0 var(--convexity-ev-consensus-only);
}

.opportunity-row-dual .matchup {
  color: var(--convexity-ev-consensus-only) !important;
  font-weight: 700;
}

.opportunity-row-dual .opportunity-status-dual {
  color: #e6c068 !important;
}

.opportunity-row-dual:hover td {
  background-color: #4a3a1a !important;
}

.opportunity-row-dual .opportunity-ev-badge {
  background: var(--convexity-ev-consensus-only);
  color: #1a1200;
}

.opportunity-ev-badge {
  display: inline-block;
  margin-left: 0.5rem;
  padding: 0.15em 0.5em;
  border-radius: 0.3rem;
  font-size: 0.75rem;
  font-weight: 700;
  background: var(--convexity-ev-high);
  color: #04140a;
  vertical-align: middle;
}

/* -------------------------------------------------------------------------
   Models page
   ------------------------------------------------------------------------- */

.models-page {
  max-width: 42rem;
  padding-top: 2rem;
  padding-bottom: 3rem;
}

.help-price-example {
  display: inline-block;
  background: var(--convexity-bg);
  border: 1px solid var(--convexity-border);
  border-radius: 0.4rem;
  padding: 0.6rem 1.1rem;
  text-align: center;
  min-width: 8rem;
  flex-shrink: 0;
}

.golden-text {
  color: var(--convexity-ev-consensus-only);
  font-weight: 700;
}

.model-card {
  background-color: var(--convexity-card-bg);
  border: 1px solid var(--convexity-border);
  border-radius: 0.5rem;
  padding: 1.25rem 1.5rem;
  margin-bottom: 1rem;
}

.model-card h2 {
  color: #9ad4ff;
  margin-bottom: 0.5rem;
}

.model-card p {
  color: var(--convexity-text);
  margin-bottom: 0;
  line-height: 1.55;
}

.model-banner {
  display: inline-block;
  padding: 0.4rem 1.1rem;
  border-radius: 0.4rem;
  background: #1a2a3a;
  border: 1px solid #2d4a68;
  color: #9ad4ff;
  font-size: 0.9rem;
  font-weight: 600;
  white-space: nowrap;
}

.model-banner[title] {
  cursor: help;
}

/* True center of the row, regardless of how wide the sibling controls
   are - a flex "leftover space" centering (flex-grow + text-center)
   looks centered only when the controls before it happen to be
   symmetric, and drifts left/right otherwise, especially on wide
   screens where the leftover space itself is far from the row's middle.
   (2026-09-09: briefly replaced with a fixed-position floating badge so
   the model name stayed visible after scrolling past the controls on
   Book View - reverted the same day once the frozen-header layout below
   made that unnecessary: the controls row, and this banner with it,
   never scroll out of view on desktop now.) */
.model-banner-centered {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

.model-banner strong {
  color: #fff;
}

.week-lock-label {
  font-size: 0.85rem;
  font-weight: 600;
  padding: 0.3rem 0;
  color: #f0f0f0;
}

.book-col-link {
  display: inline-block;
  margin-top: 0.15rem;
  font-size: 0.68rem;
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: #7a9fd9;
}

.book-col-link:hover {
  color: #a8c4ef;
}

/* -------------------------------------------------------------------------
   Free-tier model selector (game.html)
   ------------------------------------------------------------------------- */

.free-models-card {
  background-color: var(--convexity-card-bg);
  border: 1px solid var(--convexity-border);
  border-radius: 0.4rem;
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
}

.free-model-select {
  width: auto;
  display: inline-block;
}

.model-cycle-speed {
  width: 100px;
}

/* book.html only: hangs below the Model select without adding to that
   column's flex-item height, so it doesn't throw off align-items-end's
   shared bottom baseline with the row's other dropdowns/buttons (that
   baseline is exactly why plain in-flow placement here looked "awkward" -
   it silently made this one column taller than the rest, pushing its own
   select up out of line with Sportsbook/Look ahead/Model no longer being
   parallel). The row itself carries matching bottom clearance so this
   doesn't overlap the content below it. */
.model-cycle-row {
  position: absolute;
  top: 100%;
  left: 0;
  margin-top: 0.25rem;
  white-space: nowrap;
}

/* Absolutely-positioned .model-cycle-row above doesn't add to this row's
   own flow height, so without this the cycle row would overlap whatever
   comes right after it (the game table, now that book.html's model
   banner moved inline into this row instead of sitting below it).
   2.75rem is a tight-but-safe fit for the cycle row's own ~2rem height
   (a btn-sm button is its tallest element) plus its 0.25rem margin-top
   above - trimmed down from a looser 3rem, since every bit of header
   height matters on a shorter viewport like an iPad. */
.book-controls-row {
  margin-bottom: 2.75rem !important;
}

/* Frozen-header Book View layout (desktop only, 2026-09-09, Paula's
   request): the title, controls row (model banner now inline in it - see
   above), and outline legend stay fixed on screen, and only the game grid
   underneath scrolls - fixes losing track of which book/model you're
   looking at after scrolling a long weekly slate. The descriptive
   paragraph that used to sit above the controls row was removed the same
   day for the same reason: less fixed header, more room for the grid.
   Mobile is untouched (same min-width:576px desktop
   threshold as the table/card-view split below) - its stacked-card view
   is usually much shorter per game, and a fixed-height body fights with
   mobile browsers' own show/hide-on-scroll chrome.
   .frozen-table-page pins body to exactly one viewport tall with no page
   scroll of its own; .frozen-table-main overrides the site-wide
   `main { flex: 1 0 auto }` (which never shrinks below its content, so
   the page just grows past 100vh and scrolls normally) to flex-shrink
   instead, filling exactly whatever's left between the sticky navbar and
   footer. .frozen-table-scroll itself does NOT scroll (any loading/empty
   state and the outline legend sit in it at their natural size) - the
   actual overflow-y:auto lives one level deeper, directly on
   #book-table-wrap, because #book-table-wrap is already Bootstrap's
   .table-responsive (overflow-x:auto for narrow windows). Per the CSS
   overflow spec, an element with any non-visible overflow-x forces its
   overflow-y to a non-visible value too, so .frozen-table-scroll itself
   was silently becoming ITS OWN nearest scrolling ancestor for nothing -
   .table-dark-custom thead th's position:sticky (see that rule) sticks
   to whichever ancestor actually scrolls, and #book-table-wrap never
   scrolled on its own until this rule gave it one - so the sticky
   column headers had nothing to stick to and just scrolled away with
   the rows. Putting the real overflow-y directly on #book-table-wrap
   makes it correctly both the horizontal AND vertical scrolling
   ancestor, which is what the sticky thead needs. min-height:0
   everywhere here is the standard flexbox fix that lets a flex child's
   overflow work at all instead of forcing its parent to grow to fit it. */
@media (min-width: 576px) {
  body.frozen-table-page {
    /* 100vh on mobile Safari (iPad included) measures the viewport as if
       its address/toolbar chrome were always hidden - the LARGEST the
       page could ever get - not the actually-visible height once that
       chrome is showing. Since this body has overflow:hidden (no page
       scroll of its own), the gap between that inflated 100vh and the
       real visible height doesn't reveal itself as scrollable content -
       it just gets clipped off the bottom, silently shrinking whatever
       is visible of the flex-filled table area (.frozen-table-main below)
       while the header above keeps its full, unaffected height. Net
       effect: the header reads as consuming a much larger share of the
       actually-visible screen than its own height alone would suggest -
       confirmed on an iPad, 2026-09-09. 100dvh (dynamic viewport height)
       tracks the real, current visible height as that chrome shows/hides;
       declared after 100vh so a browser that doesn't understand dvh
       simply ignores this line and keeps the 100vh fallback above it. */
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
  }

  .frozen-table-main {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
  }

  .frozen-table-scroll {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
  }

  .frozen-table-scroll #book-table-wrap {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
  }
}


/* -------------------------------------------------------------------------
   Brand card - shared "centered card with logo/tagline copy" treatment
   used by both the public landing page (index.html) and the /paula sign-in
   form (paula.html). Was originally login-page-only styling; renamed when
   the public login gate was removed and this shape was reused for the
   landing hero.
   ------------------------------------------------------------------------- */

.brand-card-page {
  max-width: 32rem;
  padding-top: 4rem;
  padding-bottom: 4rem;
}

.brand-card {
  background-color: var(--convexity-card-bg);
  border: 1px solid var(--convexity-border);
  border-radius: 0.5rem;
  padding: 2rem;
}

.brand-logo {
  height: 2.6rem;
  width: auto;
  margin-bottom: 0.75rem;
}

.brand-banner {
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: 0.02em;
}

.brand-tagline {
  color: #9ad4ff;
  font-weight: 600;
  margin-bottom: 1rem;
}

.brand-intro {
  color: var(--convexity-text);
  font-size: 0.92rem;
  line-height: 1.5;
}

.brand-disclaimer {
  color: #999;
  font-size: 0.78rem;
  margin-bottom: 1.5rem;
}

.brand-error {
  color: var(--convexity-ev-negative);
  font-size: 0.85rem;
  margin-bottom: 0.75rem;
}

/* -------------------------------------------------------------------------
   Sport navigation grid (landing page)
   ------------------------------------------------------------------------- */

.sport-nav-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: 0.75rem;
  margin-top: 1.5rem;
}

.sport-pill {
  display: block;
  text-align: center;
  padding: 0.85rem 0.5rem;
  border-radius: 0.5rem;
  background-color: var(--convexity-bg);
  border: 1px solid var(--convexity-border);
  font-weight: 600;
  text-decoration: none;
  color: var(--convexity-text);
  transition: border-color 0.15s ease, color 0.15s ease;
}

.sport-pill:hover {
  border-color: #8ab4ff;
  color: #fff;
}

.sport-pill-disabled {
  color: #666;
  cursor: default;
  user-select: none;
}

.sport-pill-disabled:hover {
  border-color: var(--convexity-border);
  color: #666;
}

.sport-pill-soon {
  display: block;
  margin-top: 0.25rem;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #888;
}

/* -------------------------------------------------------------------------
   Book View heading
   ------------------------------------------------------------------------- */

/* "Book View" (see book.html) stays left-justified in normal flow, and the
   descriptive note stays right-justified in normal flow (flex,
   justify-content:space-between) - book-name-heading is absolutely
   centered against the row independently of both, same technique as
   .navbar-center-title above, so neither side's text ever shifts it off
   true center. 2026-09-09, Paula: the note moved back in from its own row
   below the controls, now beside the name instead, to keep the frozen
   header on Book View as short as possible; the "Showing: X Model" banner
   sits on its own short centered line right below this row (see
   .model-banner-row) rather than being stacked inside this one - putting
   the book name parallel with "Book View" instead of below it saves a
   whole line, and keeping the banner as a genuine in-flow sibling (rather
   than packed into this row's own absolutely-positioned block) avoids it
   ever visually overflowing past this row's height into the controls
   below, which a taller two-line centered block risked.

   "Back to Games" (.book-back-link) briefly rode on this same line beside
   "Book View" - reverted the same day: align-items:baseline (needed to
   line the two up) baseline-aligns against book-title-note's LAST line
   once that note wraps to two-plus lines, which pushes "Book View" down
   inside the row and left a visible gap of empty space above it - a
   worse trade than the line it saved. align-items is back to flex-start
   so "Book View" always sits flush against the row's own top regardless
   of how tall the note wraps. "Back to Games" itself now shares a line
   with the "Showing: X Model" banner instead (see .model-banner-row) -
   that pairing has no note to wrap against, so it saves the same line
   without the same risk. .book-title-row is now the very first thing in
   <main> (no back-link line above it anymore), so its share of
   .page-title's normal 1.5rem top margin is trimmed here specifically -
   every other page keeps the full 1.5rem via the untouched shared rule. */
.book-title-row {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 0.5rem;
}

/* "Back to Games" (small caption) and the "Showing: X Model" banner
   (centered) now share this one line, directly under .book-title-row -
   NOT sharing a line with "Book View" itself (see .book-title-row's own
   remarks for why that specific pairing broke: baseline-aligning against
   book-title-note's wrapped last line pushed "Book View" down and left a
   gap above it). This line has no such note to wrap against, so pairing
   the back-link with the banner instead saves the same line safely.
   Kept the id/class name "model-banner-row" (no JS depends on the name
   itself, only on #model-banner's own content) rather than rename it
   everywhere just because a second thing now lives in it. */
.model-banner-row {
  position: relative;
  display: flex;
  align-items: center;
  margin-bottom: 0.75rem;
}

.book-back-link {
  font-size: 0.85rem;
}

/* Absolutely centered against .model-banner-row independently of
   .book-back-link's own width, same technique as .book-name-heading
   below - so the pill stays centered under the book name regardless of
   how long "Back to Games" (or a sport's name, e.g. "Back to NCAA
   Basketball Games") happens to be. */
.model-banner-row .model-banner {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
}

.book-name-heading {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-weight: 700;
  white-space: nowrap;
}

.book-title-note {
  max-width: 26rem;
  text-align: right;
  line-height: 1.4;
}

/* State book-filter mode: a local/tribal venue's "LOCAL" or "LOCAL -
   <AFFILIATE>" line on book.html's own big page heading - block so it
   breaks onto its own line under the (shortened) venue name, same
   text/color treatment as .mobile-stack-title-local elsewhere. */
.book-name-heading-local-line {
  display: block;
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: #999;
  margin-top: 0.15rem;
}

.book-name-badge {
  display: inline-block;
  margin-left: 0.5rem;
  padding: 0.15em 0.5em;
  border-radius: 0.25rem;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  vertical-align: middle;
  background: #1a2a3a;
  color: #9ad4ff;
  border: 1px solid #2d4a68;
}

.preseason-badge {
  display: inline-block;
  margin-right: 0.45rem;
  padding: 0.1em 0.4em;
  border-radius: 0.25rem;
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  background: #3a2f14;
  color: #f0d58a;
  border: 1px solid #6b5420;
  vertical-align: middle;
}

/* -------------------------------------------------------------------------
   book.html — odds-history / sparklines (collapsible per-game section)
   ------------------------------------------------------------------------- */

.odds-history-toggle-col {
  width: 2rem;
}

/* Per-row toggle button (leftmost column) - same ▸ / rotate-90-when-open
   idiom as .week-header::before on games.html, just on a real <button>
   instead of a <details> marker, since book.html's rows are plain <tr>s,
   not <details> elements (see project memory: the collapsible mechanism
   needed its own design here, not a reuse of that pattern). */
.odds-history-toggle {
  background: none;
  border: none;
  color: #888;
  font-size: 0.9rem;
  line-height: 1;
  padding: 0.2rem 0.4rem;
  cursor: pointer;
  transition: transform 0.15s ease, color 0.15s ease;
}

.odds-history-toggle:hover {
  color: #ccc;
}

.odds-history-toggle.odds-history-toggle-open {
  transform: rotate(90deg);
  color: #ccc;
}

.odds-history-expand-all-btn {
  background-color: var(--convexity-card-bg);
  border: 1px solid var(--convexity-border);
  color: #ccc;
  white-space: nowrap;
}

.odds-history-expand-all-btn:hover {
  border-color: #666;
  color: #fff;
}

/* The expanded detail row - one wide cell (colspan across every real
   column) styled like a nested card, matching .card-dark's own
   background/border tokens so it reads as "content that belongs to the
   row above it," not a visually disconnected panel. */
.odds-history-row td {
  background-color: #161616 !important;
  border-top: none !important;
  padding: 0.75rem 1rem 1rem 2.25rem;
}

.odds-history-props {
  display: grid;
  /* Wide enough that a full changelog line ("Tue, Aug 25, 12:00 PM —
     +155 (39.2%)") never wraps - 220px was too tight and forced awkward
     mid-line wraps at the card's own font size. min(320px, 100%) so that
     minimum still shrinks to fit on a phone narrower than 320px of
     available width instead of overflowing it. */
  grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
  gap: 0.75rem 1.5rem;
}

.odds-history-prop {
  border: 1px solid var(--convexity-border);
  border-radius: 0.35rem;
  padding: 0.5rem 0.65rem;
  background-color: var(--convexity-card-bg);
}

.odds-history-prop-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
}

.odds-history-prop-label {
  font-weight: 600;
  font-size: 0.8rem;
  color: #ccc;
}

/* Price-history line chart (priceHistoryChartHtml in app.js) - width is
   set inline per-proposition (proportional to real elapsed tracked time),
   so this only supplies vertical alignment/spacing, not sizing. */
.price-history-chart-wrap {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  vertical-align: middle;
}

.price-history-chart {
  display: inline-block;
  vertical-align: middle;
}

.price-history-delta {
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
}

/* paula-anomaly-watchlist.html's dual-listbox picker - the two <select
   multiple> boxes and the Add/Remove button column between them. A fixed
   height (not 100%) is deliberate: percentage height on a <select> inside
   a stretched flex column measured wrong in practice, collapsing the row's
   own box while the <select> still painted at its full natural size -
   which visually overlapped the Save button sitting right after the row
   (2026-08-28, reported by Paula: "save button sits on top of the All
   books list"). A real height avoids that circularity outright.
*/
.dual-listbox-select {
  height: 22rem;
}

.dual-listbox-controls {
  padding-top: 1.75rem;
}

.dual-listbox-save-row {
  margin-top: 6.5rem;
}

.odds-history-list {
  list-style: none;
  padding-left: 0;
  margin: 0;
  max-height: 7.5rem;
  overflow-y: auto;
  font-size: 0.75rem;
  color: #bbb;
}

.odds-history-list li {
  padding: 0.1rem 0;
  border-bottom: 1px dotted #2a2a2a;
  white-space: nowrap;
}

.odds-history-list li:last-child {
  border-bottom: none;
}

.odds-history-loading {
  color: #888;
  font-size: 0.85rem;
  font-style: italic;
}

/* -------------------------------------------------------------------------
   best-odds.html (and per-sport equivalents) — "Best Odds" quick-glance
   card: whichever book currently has the single best price per side, not
   a full per-book comparison grid.
   ------------------------------------------------------------------------- */

.best-odds-game {
  margin-bottom: 1.5rem;
}

.best-odds-game-header {
  margin-bottom: 0.5rem;
}

.best-odds-game-matchup {
  font-weight: 700;
  font-size: 1.05rem;
  color: var(--convexity-text);
}

.best-odds-game-kickoff {
  color: #999;
  font-size: 0.85rem;
}

/* A real section heading (game.html's embedded placement) rather than a
   small muted label - the section reads as an afterthought otherwise. */
.best-odds-section-label {
  font-size: 1.3rem;
  font-weight: 700;
  color: #f0f0f0;
  display: inline-block;
  margin-bottom: 0.5rem;
}

/* Wraps the label together with the free-model win-probability line so
   they read as one glance-value stack, centered above the card (matching
   its 640px cap) rather than the label sitting flush against the page
   edge while the card floats centered below it. Label stacks above the
   predicted-winner line, not side by side - it's the section's title,
   so it reads first and largest. */
.best-odds-header-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  max-width: 640px;
  margin: 0 auto;
  text-align: center;
}

.best-odds-header-row .best-odds-section-label {
  margin-bottom: 0;
  font-size: 1.6rem;
}

.best-odds-header-row .recommendation {
  font-size: 1.05rem;
  font-weight: 600;
}

.best-odds-header-row .recommendation .homeProb,
.best-odds-header-row .recommendation .awayProb {
  display: inline-block;
  margin-right: 1.25rem;
  color: #9ad4ff;
}

.best-odds-header-row .recommendation .awayProb {
  margin-right: 0;
}

/* Accent border + subtle glow so the card reads as a distinct, highlighted
   feature rather than blending into the page background - the plain
   border/card-bg combo it had before was too close in value to the
   surrounding dark page to register as its own section.
   max-width caps it as a floating card rather than stretching edge-to-
   edge on game.html's container-fluid (no width constraint of its own) -
   full-width made the gap between a short book name and its price look
   like an unintentional layout gap on a wide monitor, not a designed
   card. Same cap applies on the Tools page too, where it works alongside
   the existing grid (best-odds-list) rather than against it. */
.best-odds-card {
  max-width: 640px;
  margin-left: auto;
  margin-right: auto;
  border: 1px solid var(--convexity-ev-model);
  border-radius: 0.5rem;
  overflow: hidden;
  background-color: #181c22;
  box-shadow: 0 0 0 1px rgba(59, 158, 255, 0.12), 0 6px 18px rgba(0, 0, 0, 0.4);
}

.best-odds-section + .best-odds-section {
  border-top: 1px solid var(--convexity-border);
}

/* Purple (--convexity-parlay) deliberately - red/green/blue are all
   already spoken for by the opportunity-outline system elsewhere on this
   card, so a section header in any of those would misleadingly suggest
   the section itself is "an opportunity." Reuses the same violet already
   used for the Parlay feature rather than introducing an unrelated new
   color to the palette. */
.best-odds-section-header {
  background: linear-gradient(180deg, #202836, #171b22);
  color: var(--convexity-parlay);
  font-weight: 700;
  padding: 0.6rem 0.9rem;
  font-size: 0.95rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  border-bottom: 1px solid rgba(169, 112, 255, 0.3);
}

/* Team/line label stacked above a book+price line, rather than all three
   crammed onto one row - found via a text-wrapping bug (a long team name
   plus a wide book badge overflowed the price outside the card), but
   Paula's own call afterward: this reads better regardless, not just as
   an overflow fix. */
.best-odds-row {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  padding: 0.6rem 0.9rem;
}

.best-odds-row + .best-odds-row {
  border-top: 1px solid var(--convexity-border);
}

/* The proposition (team, or Over/Under) is the primary thing being
   identified here - sized up from the book name so the visual hierarchy
   reads label > book > (price stays the largest/boldest of all three,
   it's the actual number being shopped for). */
.best-odds-label {
  font-weight: 700;
  font-size: 1.1rem;
  color: var(--convexity-text);
}

.best-odds-row-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
}

.best-odds-book {
  color: #ddd;
  font-size: 0.9rem;
  font-weight: 600;
}

/* Deliberately NOT the blue opportunity-model accent color - every price
   on this card used to be blue unconditionally, which diluted the same
   blue used for the model-only outline (opportunityOutlineClass): a card
   with several rows read as "a lot of blue" even when only one or two
   were genuinely flagged as an opportunity, because the accent no longer
   meant anything distinct from "this is a price." Neutral off-white here
   keeps blue reserved exclusively for a real signal. */
.best-odds-price {
  font-weight: 800;
  font-size: 1.45rem;
  color: var(--convexity-text);
  flex: 0 0 auto;
  min-width: 3.5rem;
  text-align: right;
}

/* Cards read fine full-width on mobile, but on wider screens a single
   column of cards wastes horizontal space - lay them out in a responsive
   grid instead once there's room for more than one. */
.best-odds-list {
  display: grid;
  /* min(380px, 100%) rather than a bare 380px - on a phone narrower than
     380px (e.g. iPhone SE at 375pt) a bare minmax minimum forces the
     single column wider than the viewport instead of shrinking to fit,
     which overflows the page horizontally. */
  grid-template-columns: repeat(auto-fill, minmax(min(380px, 100%), 1fr));
  gap: 1.25rem 1.5rem;
  align-items: start;
}

/* odds-anomalies.html - single-column stack, not a grid like
   .best-odds-list, since each card's Reason sentence needs the full
   width to read comfortably. */
.anomaly-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.anomaly-card {
  padding: 0.85rem 1rem;
  border-radius: 0.4rem;
}

/* Same "dim, don't recolor" transparency cue as .book-col-stale/
   .stack-section-stale elsewhere - a corrected anomaly isn't wrong to show,
   just no longer live. */
.anomaly-card-resolved {
  opacity: 0.7;
}

.anomaly-card-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.anomaly-severity {
  font-weight: 700;
  font-size: 0.85rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.anomaly-sport-badge {
  display: inline-block;
  padding: 0.1em 0.5em;
  border-radius: 0.25rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  background: #262626;
  color: #ccc;
  border: 1px solid var(--convexity-border);
}

.anomaly-resolved-badge {
  display: inline-block;
  padding: 0.1em 0.5em;
  border-radius: 0.25rem;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  background: #1a2e1a;
  color: #8fd68f;
  border: 1px solid #2f4a2f;
}

.anomaly-time {
  margin-left: auto;
}

.anomaly-game {
  font-weight: 700;
  color: var(--convexity-text);
  margin-bottom: 0.2rem;
}

.anomaly-proposition {
  margin-bottom: 0.3rem;
}

.anomaly-check {
  margin-bottom: 0.4rem;
}

.anomaly-reason {
  color: #ccc;
  font-size: 0.9rem;
  line-height: 1.4;
}

/* -------------------------------------------------------------------------
   Phone breakpoint (portrait iPhone SE through 17 Pro Max all fall
   between 375-440 logical pt, comfortably under Bootstrap's 576px `sm`
   breakpoint - a landscape phone is 850pt+ and gets the desktop layout,
   same as the iPad-landscape case that already looks right unmodified).
   ------------------------------------------------------------------------- */

@media (max-width: 575.98px) {
  /* game.html's #game-table-wrap (side-by-side book columns) and book.html's
     #book-table-wrap (side-by-side market columns) both have a stacked-list
     sibling (#game-book-sections / #book-game-sections - see game.js's
     renderBookSections and book.js's renderGameSections) that's always
     rendered from the same data; only one of each pair is shown per
     viewport width. Plain display:none on the unique IDs rather than
     Bootstrap's d-sm-* utilities, because both pages' JS already toggles a
     bare .d-none on these elements for the loading/no-odds states -
     stacking a second responsive .d-none on top of that would get
     clobbered every time JS added or removed the first one. */
  #game-table-wrap,
  #book-table-wrap {
    display: none !important;
  }

  /* free-models-card's banner is normally absolutely centered over its
     row (.model-banner-centered - see that rule above), which only reads
     right when the row stays on one line. On a phone the row wraps
     (model select on its own line), and the absolutely-positioned banner
     then centers over the row's full wrapped height instead of sitting
     next to anything - landing on top of the select. Dropping back into
     normal flow avoids the overlap. */
  .model-banner-centered {
    position: static;
    transform: none;
    display: block;
    margin-top: 0.5rem;
    text-align: center;
  }

  /* Same absolute-centered-over-a-row collision as .model-banner-centered
     above, on book.html's page title instead: .book-name-heading is
     normally centered over the whole .book-title-row independently of the
     "Book View" text and the descriptive note beside it (see that rule's
     own comment), which reads fine on desktop but on a phone leaves no
     room for any of the three, so they'd land right on top of each other
     (Paula, 2026-08-31). Dropping out of absolute positioning puts the
     name on its own line right after "Book View" instead, per her
     request; the note (already hidden below the lg breakpoint - see
     .book-title-note) never reaches this rule at all. */
  .book-name-heading {
    position: static;
    transform: none;
    display: block;
    margin-top: 0.25rem;
    text-align: left;
  }

  /* book.html's per-game price-history sparklines have no equivalent in
     the mobile stacked view (#book-game-sections) yet - the button would
     still work, but against the desktop table sitting hidden underneath
     it, with nothing visible changing. Hiding it avoids that dead control
     rather than building the feature twice for this first pass - the
     .odds-history-mobile-note next to it (hidden on desktop below) takes
     its place instead, so the gap reads as intentional, not missing. */
  #odds-history-expand-all-btn {
    display: none;
  }

  .odds-history-mobile-note {
    display: inline-block !important;
  }

  /* games.html's week tables - unlike game.html/book.html above, this
     content (kickoff, matchup, score/status) is simple enough not to need
     a second JS-rendered view; a pure CSS reflow of the same <tr>/<td>
     markup is enough. Each row becomes a flex container so its 3 real
     cells (kickoff, matchup, status - .game-row-spacer stays a plain
     table cell but is hidden below, its job of absorbing leftover width
     is moot once every cell is forced to its own full-width line) can be
     stacked as their own lines, reordered so the matchup reads first and
     the kickoff time second, per Paula's request - the DOM order (kickoff,
     matchup, status) stays what makeSortable/screen readers see, only the
     visual order changes. */
  .week-block table tr {
    display: flex;
    flex-wrap: wrap;
  }

  /* :not(.game-row-spacer) matches the exact selector shape of the
     unconditional ".week-block table td:not(.game-row-spacer) { white-space:
     nowrap }" rule above (outside this media query) so this override beats
     it on specificity (both are 2 classes/pseudo-classes; type-count is the
     tiebreaker) - a plain ".week-block table tr > td" here has one fewer
     class-equivalent selector than that rule and loses the cascade despite
     appearing later in the file, silently leaving nowrap in effect. */
  .week-block table tr > td:not(.game-row-spacer) {
    flex: 1 1 100%;
    white-space: normal;
  }

  .week-block table tr > td:nth-child(1) {
    order: 2;
    color: #9c9c9c;
    font-size: 0.85rem;
    padding-top: 0;
  }

  .week-block table tr > td.matchup {
    order: 1;
  }

  .week-block table tr > td.game-status-cell {
    order: 3;
    font-size: 0.85rem;
    padding-top: 0;
  }

  .week-block table tr > td.game-row-spacer {
    display: none;
  }

  /* Parlay offcanvas (book.html): the desktop clamp() width set on
     #parlayOffcanvas above was deliberately tuned to reveal specific
     *grid columns* behind it (Spread Away, per that rule's own comment) -
     a column-preservation goal that doesn't exist against the mobile
     stacked-card view (#book-game-sections has no columns to protect), so
     it's free to go narrower here. Also translucent rather than solid:
     data-bs-backdrop="false" already keeps the cards live/tappable
     underneath the panel, but on a phone the panel covers most of the
     screen width, so a fully opaque background hid which proposition was
     about to be tapped (Paula, 2026-08-31) - alpha tuned to keep the
     panel's own text legible while still letting the covered card show
     through enough to aim a tap. Must stay an ID selector, not a class,
     for the same specificity reason already documented on #parlayOffcanvas. */
  #parlayOffcanvas {
    width: min(70vw, 16rem);
  }

  .parlay-offcanvas {
    /* 0.88 -> 0.7 -> 0.5 (Paula, 2026-08-31) - blur(2px) does more of the
       "keep the panel's own text legible" work now that the background
       itself lets more through. */
    background-color: rgba(30, 30, 30, 0.5);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
  }
}

@media (min-width: 576px) {
  #game-book-sections,
  #book-game-sections {
    display: none !important;
  }
}

/* -------------------------------------------------------------------------
   Mobile stacked view - shared by game.html's #game-book-sections (one card
   per book, each listing that book's priced markets - see renderBookSections
   in game.js) and book.html's #book-game-sections (one card per game, each
   listing that game's Spread/Moneyline/Total for the currently-selected
   book - see renderGameSections in book.js). Same "list one after another
   instead of side by side" metaphor in both places, just transposed which
   axis (book vs. game) becomes the card and which becomes the rows within
   it - so both share this one set of card/row styles rather than
   duplicating them per page.
   ------------------------------------------------------------------------- */

.mobile-stack-section {
  border-radius: 0.5rem;
  overflow: hidden;
}

#game-book-sections,
#book-game-sections {
  margin-bottom: 0.85rem;
}

/* Explicit divider between consecutive cards (one book on game.html, one
   game on book.html) - a real element in the gap between two cards, per
   Paula's feedback, rather than relying on the cards' own border + margin
   alone to read as "this book/game ended, a new one starts here" on an
   actual phone screen. Rendered by both renderBookSections (game.js) and
   renderGameSections (book.js), joined between cards instead of "". */
.mobile-stack-divider {
  border: none;
  border-top: 1px solid var(--convexity-border);
  margin: 0.85rem 0;
}

.mobile-stack-header {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  column-gap: 0.5rem;
  row-gap: 0.15rem;
  padding: 0.6rem 0.85rem;
}

.mobile-stack-title {
  font-weight: 700;
  color: #f0f0f0;
}

/* State book-filter mode: a local/tribal venue's "LOCAL" or "LOCAL -
   <AFFILIATE>" line, forced onto its own line below the venue name (see
   .mobile-stack-note, the same block/small/muted treatment used for the
   "as of ..." staleness note elsewhere in this same header). */
.mobile-stack-title-local {
  display: block;
  width: 100%;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: #999;
}

/* Same "dim, don't recolor" treatment as #game-table-wrap th.book-col-stale
   on the desktop grid - a stale book isn't wrong, just not fresh enough to
   trust at a glance. */
.mobile-stack-header.stack-section-stale .mobile-stack-title {
  opacity: 0.55;
}

.mobile-stack-note {
  display: block;
  width: 100%;
  font-size: 0.75rem;
  color: #999;
}

.mobile-stack-rows {
  border-top: 1px solid var(--convexity-border);
}

.mobile-stack-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.55rem 0.85rem;
}

/* Faint boundary where the market family changes (Spread -> Moneyline ->
   Total) - deliberately fainter than .mobile-stack-divider between whole
   cards above, and deliberately absent between two rows of the *same*
   family (e.g. Spread Away/Spread Home), which should read as one group.
   Same dotted idiom already used for .odds-history-list li's separators
   elsewhere on this page. Applied via .mobile-stack-row-family-start,
   computed by the caller (game.js/book.js) from each row's own family
   tag, not by a plain adjacent-sibling selector - CSS alone can't compare
   a class value between two siblings. */
.mobile-stack-row-family-start {
  border-top: 1px dotted #2a2a2a;
}

.mobile-stack-row-label {
  min-width: 0;
}

.mobile-stack-row-line {
  display: block;
  font-weight: 600;
  color: #f0f0f0;
}

.mobile-stack-row-fair {
  display: block;
  margin-top: 0.1rem;
}

.mobile-stack-row-price {
  flex-shrink: 0;
  text-align: right;
}

.mobile-stack-row-price-value {
  font-weight: 700;
  font-size: 1.05rem;
}
