/* Betfair-style palette (2026-08-16, Mark's reference) -- yellow as the
   DOMINANT page background, not a sparing accent: black header/cards/tab
   bar float on top of it, white text on those black surfaces, the same
   yellow reused for CTAs/accents/odds so it reads as one deliberate brand
   color, not decoration. Mobile-shaped chrome underneath: fixed app bar +
   bottom tab bar, full-viewport-height screens, single column, big tap
   targets -- not trying to also look reasonable at desktop width,
   betslip-widget/demo already covers that shape, this is a separate page. */
:root {
  color-scheme: dark;
  --yellow: #f5a623;
  --bg: var(--yellow);
  --ink: #1a1200;      /* dark text sitting directly on the yellow background */
  --card: #161616;     /* black cards floating on the yellow page */
  --surface: #242424;  /* nested tappable elements inside a black card */
  --border: #2c2c2c;
  --fg: #f5f5f5;        /* text on black surfaces */
  --muted: #9a9a9a;
  --danger: #e2607a;    /* loss/debit -- the one non-yellow semantic color, kept for a clear negative signal */
  --up: #34d399;        /* odds-drift-up arrow (2026-08-16, Mark: red/green arrows, same as betslip-widget/demo) -- down reuses --danger, this is the only place green appears in this otherwise yellow/black/red palette */
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--fg);
  font-family: -apple-system, "Segoe UI", Roboto, sans-serif;
  /* Belt-and-suspenders (2026-08-21) -- the real fix is the pull-to-refresh
     JS wired to #betsList/#activityList below, which already preventDefault()s
     the gesture it owns, but this stops the browser's native pull-to-refresh
     (a full page reload, which lands back on #screen-offers since that's the
     only non-`hidden` section in the raw document) from ever reaching the
     viewport if some browser still tries to chain the scroll this far up. */
  overscroll-behavior-y: contain;
}

.app {
  display: flex;
  flex-direction: column;
  height: 100dvh;
  max-width: 480px;
  margin: 0 auto;
  position: relative;
  overflow: hidden;
  background: var(--bg);
  /* Real bug reported (2026-08-21, Mark): a horizontal drag on Featured,
     Bets or Wallet -- none of which scroll sideways at all, confirmed;
     only .odds-sports-grid below does -- was never consumed by anything
     in this app, so it fell straight through to the phone browser's own
     edge-swipe gesture (switches to an adjacent OPEN BROWSER TAB, or just
     rubber-bands when there's no tab further in that direction -- exactly
     "cambia a la seccion aledaña ... si no hay, un rebote"). `touch-action`
     is the standards-based way to opt a whole subtree out of native
     horizontal gesture handling while keeping vertical scroll (`pan-y`)
     fully native everywhere else in the app. Inherited by every screen
     below; .odds-sports-grid opts back into `pan-x` since IT is the one
     place a native horizontal drag should still do something. */
  touch-action: pan-y;
}

.appbar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.1rem;
  /* Vertical padding trimmed 2026-08-23 (Mark: "un 10% menos alto la
     franja del menu superior") -- was 0.9rem, no explicit height on this
     bar so its total rendered height is emergent from this padding plus
     the brand/search-input row's own line height. Horizontal padding
     trimmed the same day, separately, for "+15% ancho" on the search box. */
  padding: 0.71rem 0.1rem;
  padding-top: max(0.71rem, env(safe-area-inset-top));
  background: var(--card);
}
/* Brand icon removed (2026-08-20, Mark: "sacar el icono de la izquierda
   de oddswall para generar espacio") -- the search box (moved into the
   title bar itself, same request) needed the room. Small explicit
   margins keep the title/reset button from crowding the search box
   without eating too much into its own width (2026-08-20, Mark: "hacer
   un poco mas larga la caja de busqueda", then "agrandarla un 30%") --
   the search input is the only flex-grow item in this row, so it claims
   whatever room these two don't; brand font-size trimmed a bit too, same
   reason. */
.brand-group { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; margin-right: 0; }
.brand { font-weight: 800; font-size: 0.95rem; color: var(--yellow); } /* Grown back 2026-08-23 (Mark: "aumentar el tamaño de letra del oddswall y achicar lo que haga falta el box de busqueda") -- reverses the several rounds of trims this same day that shrank it down to 0.6rem for search-box width; the search box gives way instead now, automatically, since it's the only flex-grow item in this row (see its own flex:1 1 auto). */
/* "demo" subtitle under the brand, smaller and centered (2026-08-22, Mark)
   -- column-flex on .brand-group above (was a single-line row) stacks it
   under "oddswall" and centers both, instead of a separate absolutely-
   positioned label. */
.brand-sub { font-weight: 700; font-size: 0.52rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
/* Live SSE connection state, shown as a glow on the "o" of "oddswall"
   itself rather than a separate dot (2026-08-20, Mark: "en lugar del
   punto verde q titila sea la 'o' de oddswall la que hace ese glow en
   amarillo") -- pulses yellow while connected; plain, glow-free text for
   "reconnecting"/no active subscription, no hiding since it's a real
   letter of the word. */
.odds-live-letter.odds-live-live {
  animation: oddsLiveGlow 2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .odds-live-letter.odds-live-live { animation: none; text-shadow: 0 0 6px var(--yellow); }
}
@keyframes oddsLiveGlow {
  /* Animates the letter's own color too, not just shadow blur radius
     (2026-08-20, Mark: "es apenas visible el glow... que gradualmente
     cambie de color hasta blanco y vuelva amarillo") -- a yellow glow
     around already-yellow text barely showed; fading the glyph itself
     toward white and back is what actually reads as a pulse. */
  0%, 100% { color: var(--yellow); text-shadow: 0 0 3px var(--yellow); }
  50% { color: #fff; text-shadow: 0 0 10px #fff, 0 0 18px var(--yellow); }
}
/* flex-shrink:0 + no flex-grow keeps this always flush against the
   appbar's own right edge (2026-08-20, Mark: "el boton de reset ...
   siempre pegada a la derecha") regardless of how wide the search box
   next to it gets. */
/* Odds-tick latency + live notification rate (2026-08-22, Mark: "poner en
   pla pantala principal ... la mediciond el retraso de la notificacion" /
   "el conte de las notificaciones x segundo" / "en la barra superior,
   acortando algo el cuadro de busqueda") -- flex:0 0 auto: .odds-search-
   input is the only flex-grow item in this row (see its own flex:1 1
   auto), so it automatically narrows to make room for whatever this
   element's own content needs. min-width reserves that room UNCONDITIONALLY
   (2026-08-22, Mark: "mantener fijo el tamano de la caja de busqueda
   aunque se cambie de pestana y no tenga metricas a mostrar") -- without
   it, going empty (leaving Offers, or before the first live event ever
   arrives) frees up the space and the search box visibly grows to fill
   it, then shrinks back the instant real text returns -- a size jump on
   every tab switch. Sized for this badge's own longest REALISTIC content
   (tabular-nums makes any same-digit-count value the same width, so
   "Δ 999ms · 12.3/s · 9S" is the real ceiling on this local demo, not a
   worst-case 4-digit ms + 2-digit sessions that would need a genuinely
   broken pipeline or a small crowd of open tabs to ever occur) -- so the
   search box's width stays constant whether the badge is showing real
   text or nothing at all. Widened 2026-08-23 for the 3rd value (active
   sessions, see fetchActiveSessions() in app.js) -- narrows the search
   box a bit, which is exactly what Mark asked for ("si es necesario
   achicar un poco el cuadro de busqueda para q entre el tercer valor")
   rather than a separate change to .odds-search-input itself; re-tightened
   the same day to the realistic ceiling above once a further "+15% ancho"
   request needed the room back. */
.odds-live-stats { flex: 0 0 auto; min-width: 6.7rem; font-size: 0.68rem; color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.appbar-right { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; margin-left: 0; }
.reset-btn {
  width: 1.4rem; height: 1.4rem; padding: 0; border-radius: 999px;
  background: var(--surface); border: none; color: var(--muted);
  font-size: 0.8rem; line-height: 1; cursor: pointer;
}
.reset-btn:active { color: var(--yellow); }
/* Balance moved to the Wallet screen (2026-08-20, Mark) -- see
   .wallet-balance-value below; .appbar-balance itself no longer used. */
/* Odometer digit reels (2026-08-17, ported from betslip-widget/demo --
   see app.js's renderOdometerBalance()) -- each digit is its own vertical
   strip of 0-9, clipped to one row's height; rolling to a new value is
   just translateY()'ing the strip, which the transition set inline
   (setDigitValue()) actually animates. Row height matches
   ODOMETER_DIGIT_H in app.js -- keep the two in sync if either changes. */
/* Row height in em, not a fixed px (2026-08-20, real bug: "Available
   balance se superpone un poco sobre el monto") -- a hardcoded 19px row
   only ever matched the small font-sizes the odometer originally shipped
   at (the old header balance, the wizard's 0.88rem summary lines); the
   Wallet screen's own much bigger 1.8rem balance clipped each digit
   glyph into that same fixed 19px window regardless, cropping it and
   throwing off its baseline against the "AVAILABLE BALANCE" label above.
   em-relative height scales with whatever font-size is actually active;
   setDigitValue() in app.js measures the real rendered row height off
   the DOM instead of assuming a constant, so the translateY roll math
   stays correct at any size. */
.od-digit { display: inline-block; width: 0.62em; height: 1.15em; overflow: hidden; vertical-align: bottom; }
.od-strip { display: block; }
.od-num { display: block; height: 1.15em; line-height: 1.15em; text-align: center; }
.od-dot { display: inline-block; }

.error { color: var(--danger); font-size: 0.82rem; padding: 0.3rem 1.1rem 0; min-height: 1em; }

.screens { flex: 1 1 auto; position: relative; overflow: hidden; }
.screen {
  position: absolute; inset: 0;
  overflow-y: auto;
  /* Reserves the scrollbar's own width permanently (2026-08-17, Mark) --
     without this, content only narrows the instant a screen actually
     grows tall enough to scroll (Offers/My Bets/Activity all do this as
     more fixtures/bets/entries load), a visible width jump. Harmless on
     touch devices/overlay scrollbars, which don't take up layout space
     either way. */
  scrollbar-gutter: stable;
  /* Less top padding than before (2026-08-20, Mark: "subir tambien el
     bloque de botones de deporte un poco hacia el top", then "subir aun
     mas... hay espacio") -- sits every screen's own content closer to
     the appbar. */
  padding: 0.2rem 1.1rem 1.25rem;
}
/* My Bets: the screen title itself IS the Active/Finished filter
   (2026-08-17, Mark: "el titulo en realidad sean dos botones para listar
   las bets activas de las finalizadas") -- same pill-tab look as the
   wizard's own mode-tabs, just bigger since it stands in for a heading. */
/* Same size and position as Offers' own Prematch/Live/All filter
   (.odds-status-filter/.odds-filter-btn below) -- 2026-08-22, Mark: "los
   botones de Active y Finished en bets del mismo tamano y posicion Live y
   All en Offers", confirmed right-aligned at the SAME half-width even
   though Bets has no breadcrumb sibling to share that row with (so there's
   a deliberate empty gap on the left, matching Offers' own proportions
   exactly rather than centering or stretching to fill it). align-self
   (not margin-left:auto) since #screen-bets is a flex COLUMN -- this is
   the cross-axis equivalent of Offers' own flex row pushing its filter to
   the far end. */
/* margin-top: 0, not the 0.3rem this used to carry -- .screen's own
   0.2rem padding-top already matches Offers' .odds-breadcrumb-row (which
   has no margin-top of its own either), so the two rows sit at the exact
   same height from the top (2026-08-22, Mark: "subirlos hasta q queden en
   la misma posicion vertical de el de live en offers") -- that leftover
   0.3rem was the only thing pushing Bets' row lower. */
.bets-subtabs { display: flex; gap: 0.25rem; margin: 0 0 0.85rem; flex: 0 0 auto; width: calc((100% - 0.6rem) / 2); align-self: flex-end; }
.subtab {
  flex: 1 1 0; padding: 0.2rem 0; border: 1px solid var(--ink); background: transparent; color: var(--ink);
  opacity: 0.65; border-radius: 999px; font-weight: 700; font-size: 0.62rem; cursor: pointer;
}
.subtab.active { background: var(--ink); color: var(--yellow); opacity: 1; }
/* Active/Finished stays fixed at top, only the list below it scrolls
   (2026-08-20, Mark: "lo mismo pasa con la ventana de Bets, el scroll no
   desplaza los botones hacia arriba, quedan siempre visibles") -- same
   fixed-header/scrolling-body technique as #screen-wallet. */
#screen-bets { display: flex; flex-direction: column; overflow-y: hidden; }
/* Same scrollbar-gutter fix as #offersList/#activityList (2026-08-20) --
   Bets cards are the same .flat-list .item component, same width-jump
   risk once the list grows past one screen. */
#screen-bets > #betsList { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; scrollbar-gutter: stable; min-height: 0; overscroll-behavior-y: contain; }

/* Pull-to-refresh (2026-08-21, Mark: scrolling down on Bets/Activity was
   triggering the browser's own native pull-to-refresh instead of reloading
   THIS screen's data -- landing back on #screen-offers since that's the
   only non-`hidden` section in the raw document, i.e. "vuelve a la pantalla
   principal"). This is a plain flex sibling placed right before the
   scrolling list itself (#betsList/#activityList), collapsed to 0 height by
   default so it takes no visual space -- wirePullToRefresh() in app.js
   grows its `height` inline as the user's finger pulls, which pushes the
   list down exactly like a native pull-to-refresh would, then snaps back
   once the refresh call resolves. Kept OUTSIDE the list elements themselves
   (not a child of #betsList/#activityList) since those get their entire
   innerHTML replaced on every render -- a child here would be wiped the
   moment loadBets()/loadActivity() re-renders. */
.ptr-indicator {
  flex: 0 0 auto; height: 0; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.ptr-spinner {
  width: 22px; height: 22px; border-radius: 50%;
  border: 3px solid var(--border); border-top-color: var(--yellow);
  box-sizing: border-box;
}
.ptr-indicator.spinning .ptr-spinner { animation: ptrSpin 0.6s linear infinite; }
@keyframes ptrSpin { to { transform: rotate(360deg); } }

/* ---------- offers ---------- */
.offer-card {
  background: var(--card); border-radius: 14px;
  padding: 0.8rem 0.9rem; margin-bottom: 0.75rem; color: var(--fg);
}
.offer-head { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; margin-bottom: 0.55rem; }
.offer-teams { font-weight: 700; font-size: 0.95rem; }
.sport-icon { display: inline-flex; vertical-align: -3px; color: var(--yellow); }
.sport-icon svg { width: 1.14rem; height: 1.14rem; }
.offer-meta { color: var(--muted); font-size: 0.76rem; text-align: right; }
.offer-countdown { color: var(--muted); font-size: 0.76rem; }
.offer-countdown.critical { color: var(--danger); }
.offer-outcomes { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.odd-btn {
  position: relative;
  flex: 1 1 calc(50% - 0.25rem); min-width: 100px;
  background: var(--surface); border: none; border-radius: 10px;
  padding: 0.6rem 0.7rem; color: var(--fg); font-size: 0.82rem; text-align: left; cursor: pointer;
}
.odd-btn b { display: block; color: var(--yellow); font-size: 0.95rem; margin-top: 0.15rem; }
/* Fixed width regardless of content (2026-08-20, Mark: "la aparicion de
   las flechas ... no deberia cambiar el tamaño del box de cotizacion") --
   an empty .odd-dir (no tick yet/faded back to blank) must reserve the
   SAME space a "▲"/"▼"/"=" glyph would take, or the box visibly resizes
   the instant a tick starts/stops. */
.odd-dir { display: inline-block; width: 0.75em; text-align: center; font-size: 0.68rem; font-weight: 800; margin-left: 0.25rem; color: var(--muted); }
.odd-dir.up { color: var(--up); }
.odd-dir.down { color: var(--danger); }
.odd-btn.picked { box-shadow: inset 0 0 0 2px var(--yellow); background: #2c2408; }
.odd-btn:disabled { opacity: 0.45; }
/* One-shot flash on an actual price tick (2026-08-17, Mark) -- see
   oddFlash/ODDS_FLASH_MS in app.js for why this only ever plays once per
   real change instead of replaying on every periodic re-render. */
@keyframes oddFlashUp { from { background: rgba(52,211,153,0.4); } to { background: var(--surface); } }
@keyframes oddFlashDown { from { background: rgba(226,96,122,0.4); } to { background: var(--surface); } }
.odd-btn.flash-up { animation: oddFlashUp 0.6s ease; }
.odd-btn.flash-down { animation: oddFlashDown 0.6s ease; }
/* Winner/loser edge stripe (2026-08-18, Mark: "usemos la misma marca verde
   o roja q usamos en el historico de apuestas") -- same right-edge stripe
   as .item-winner/.item-loser below, just matching .odd-btn's own 10px
   radius instead of .item's 12px. Replaces the old ✓/✗ .result-mark span,
   which forced the odd's own <b> line taller instead of just marking the
   button. */
.odd-btn.won::after, .odd-btn.lost::after {
  content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 10px;
  border-top-right-radius: 10px; border-bottom-right-radius: 10px;
}
.odd-btn.won::after { background: var(--up); filter: brightness(0.72); }
.odd-btn.lost::after { background: var(--danger); filter: brightness(0.72); }

/* A finished fixture stays on the board, disabled, for 3s before fading
   out (2026-08-17, Mark: "inactivarlas primero ... luego un fadeout ...
   como en la demo web") -- same FT-badge/leave-animation pattern as
   betslip-widget/demo's own .event-card, ported to .offer-card's own box
   model (no border here, so the leave keyframe has none to zero out). */
.offer-card.finished { opacity: 0.55; }
.ft-badge {
  font-size: 0.64rem; font-weight: 800; padding: 0.05rem 0.4rem; border-radius: 999px;
  background: rgba(226,96,122,0.16); color: var(--danger);
}
@keyframes offerCardLeave {
  from { opacity: 1; max-height: 260px; margin-bottom: 0.75rem; }
  to { opacity: 0; max-height: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; }
}
.offer-card.leaving { animation: offerCardLeave 0.4s ease forwards; overflow: hidden; }

/* ---------- flat lists (My Bets / Activity) ---------- */
.flat-list .item {
  position: relative;
  background: var(--card); border-radius: 12px;
  padding: 0.65rem 0.8rem; margin-bottom: 0.6rem; font-size: 0.82rem; color: var(--fg);
}
/* Always exactly two lines (2026-08-20, Mark: "las cajas de activity
   siempre de dos renglones... el monto siempre justificado a la derecha,
   si hace falta abreviar el detalle con ...") -- .item-top previously had
   no layout rule at all, so a long description just wrapped in place,
   pushing the amount down onto its own line (a 3rd line) instead of
   staying pinned to the right of the (now possibly 2-line) description.
   Truncating the description with ellipsis instead keeps every activity
   card the same two-line height regardless of how long entry_desc is. */
.item-top {
  display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem;
}
.item-top > .credit, .item-top > .debit { flex: 0 0 auto; }
/* Money-type tag on Activity's own first line (2026-08-23, Mark: "no
   indica si es extraible, noextraible o freebet") -- split into its own
   flex item, NOT just concatenated text inside the single ellipsis-
   truncated span .item-top > span:first-child used to be: a long
   description would otherwise push this tag past the ellipsis cutoff and
   hide it entirely, defeating the point of always showing it. Truncation
   now lives on .item-desc-text alone; .item-desc-group is a plain flex
   wrapper (min-width:0 so it can still shrink) that keeps both pieces on
   one line, tag always fully visible. */
.item-desc-group { display: flex; align-items: baseline; gap: 0.35em; min-width: 0; flex: 1 1 auto; }
.item-desc-text { min-width: 0; flex: 1 1 auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.item-money-type { flex: 0 0 auto; white-space: nowrap; color: var(--muted); font-weight: 400; font-size: 0.85em; }
/* Winner/loser edge stripe (2026-08-17, Mark: "en vez del tilde ... pintar
   de rojo y verde una pequeña franja a la derecha del box") -- width and
   corner rounding both match the card's own 12px border-radius, so the
   stripe reads as part of the card's shape rather than a rectangle
   overlaid on top of it. Its own border-radius (not overflow:hidden on
   the card) does the rounding, so it doesn't clip .item-just-won's own
   box-shadow glow, which needs to extend past the card's edge. */
.item.item-winner::after, .item.item-loser::after, .item.item-settled::after {
  content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 12px;
  border-top-right-radius: 12px; border-bottom-right-radius: 12px;
}
/* Darker than the base --up/--danger tokens (2026-08-17, Mark) -- those
   stay untouched since they're also used elsewhere (odds direction
   arrows, credit/debit text); filter scopes the darkening to just this
   stripe while still tracking the base color if it ever changes. */
.item.item-winner::after { background: var(--up); filter: brightness(0.72); }
.item.item-loser::after { background: var(--danger); filter: brightness(0.72); }
/* Settled (2026-08-18, Mark: "a las apuestas settled, no mostrar el badge
   del estado, usar la misma tecnica del costado ... y pindar el costado de
   blanco") -- same stripe, just plain white (--fg, the same near-white
   already used for text on black surfaces) since settled isn't a win/loss
   outcome and so gets no --up/--danger color of its own. */
.item.item-settled::after { background: var(--fg); }
/* Active's own leave + Finished/Activity's own fade-in (2026-08-18, Mark:
   "hacer el fadeout y desaparicion animada para q no salgan bruscamente ...
   incorporarlas del mismo modo en la vista Finished, un fade-in animado"
   -- then, for Activity, "animar de la misma forma los movimientos ... q
   no sean bruscos") -- same collapse shape as .offer-card.leaving/
   .pick-row.leaving, just scoped to .flat-list's own .item so it doesn't
   touch those two. */
@keyframes itemLeave {
  from { opacity: 1; max-height: 300px; margin-bottom: 0.6rem; }
  to { opacity: 0; max-height: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; }
}
.flat-list .item.leaving { animation: itemLeave 0.4s ease forwards; overflow: hidden; }
@keyframes itemEnter { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
.flat-list .item.item-entering { animation: itemEnter 0.4s ease; }
.item-sub { color: var(--muted); font-size: 0.74rem; margin-top: 0.2rem; }
/* Bet description: title on its own line, then stake and (when there is
   one) the prize paid each on their OWN line below it, the status pill
   floated to the right of that whole block (2026-08-17, Mark: "usar dos
   renglones", restructured 2026-08-21 into three: "evento, stake y premio,
   cuando corresponda" -- the prize used to be appended to the end of the
   stake line instead of getting its own row). `.item-meta-lines` is the
   new stake+prize column; `.item-meta-row` now just places that column
   next to the pill rather than laying text out itself. Active bets get a
   further row underneath for the cashout controls -- the amount box is
   deliberately narrow (~25% of a full-width field) and sits immediately
   left of the button, pill-rounded to match it. */
.item-title-row { margin-bottom: 0.3rem; }
.item-meta-row { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.4rem; align-items: flex-start; }
.item-meta-lines { display: flex; flex-direction: column; gap: 0.15rem; }
.item-meta { color: var(--muted); font-size: 0.74rem; }
.item-active .item-meta-row { margin-bottom: 0.5rem; }
/* The "regret button" -- cancels this one bet, full reversal (2026-08-22,
   Mark: "un boton circular con la x ... colocar lo mas arriba y a la
   derecha q se pueda"). Same circular/tinted-red look as the wizard's own
   .remove-pick (view ticket's per-selection ×), reused here for visual
   consistency between the two "un-do this" affordances in the app.
   Absolutely positioned INTO the card's own top-right corner rather than
   laid out inline -- .item-active gets extra right padding to match, so it
   doesn't sit on top of the stake/potential-payout text ending there.
   `.flat-list .item` (two classes, specificity 0-2-0) already sets this
   card's own padding -- a bare `.item-active` (0-1-0) can never override
   it regardless of source order, so this needs the SAME two-class shape
   to actually win (verified live: without this, computed padding-right
   stayed the base 0.8rem, not 2.15rem). */
.flat-list .item.item-active { padding-right: 2.15rem; }
.bet-cancel-btn {
  position: absolute; top: 0.35rem; right: 0.35rem;
  background: rgba(226,96,122,0.16); color: var(--danger); border: none; border-radius: 999px;
  width: 1.5rem; height: 1.5rem; font-size: 0.95rem; line-height: 1; cursor: pointer;
}
.bet-cancel-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* Tap a bet row to expand its own per-selection detail (2026-08-17, Mark). */
#betsList .item { cursor: pointer; }
/* Tap an Activity row to reveal the bet(s) that caused it, right
   underneath it (2026-08-21, Mark: "desplegar las apuestas relacionadas
   con la activity debajo de la activity al seleccionarla, y contraer al
   volver a tocar") -- see renderRelatedBets()/the #activityList click
   listener in app.js. Scoped to [data-tx-idem] specifically (2026-08-22,
   Mark: deposit/withdrawal rows don't carry that attribute at all any
   more -- they can never have a linked bet, see app.js's own
   hasLinkedBet), so they read as plain, non-interactive rows instead of
   inviting a tap that always led nowhere. */
#activityList .item[data-tx-idem] { cursor: pointer; }
/* Indented and left-bordered so a revealed bet visibly nests UNDER its own
   movement rather than reading as just another row in the same list.
   Animated open/closed via the .expanded class (2026-08-22, Mark: "animar
   el despliegue y repliegue de las apuestas relacionadas en activity") --
   the CSS grid-template-rows 0fr/1fr trick, not a fixed max-height (what
   .item-detail's own animation uses, see that rule's own comment): this
   panel can legitimately hold many bet cards for a cart-wide movement, no
   safe fixed cap exists, and grid-template-rows always animates to the
   TRUE content height regardless of how tall it turns out to be. Needs
   exactly one child to size against -- app.js's setRelatedContent() always
   wraps the real content in one .activity-related-inner div for this
   reason, never writes into .activity-related's own innerHTML directly.
   min-height:0 on that inner child is required -- a grid item defaults to
   min-height:auto (its own content's height), which would otherwise never
   let the row actually shrink to 0fr. */
.activity-related {
  display: grid; grid-template-rows: 0fr;
  margin: 0 0 0 0.6rem; padding-left: 0.7rem;
  border-left: 0 solid var(--border);
  transition: grid-template-rows 0.3s ease, border-left-width 0.3s ease, margin 0.3s ease;
}
.activity-related.expanded {
  grid-template-rows: 1fr;
  margin: -0.35rem 0 0.6rem 0.6rem;
  border-left-width: 2px;
}
/* .visible is a plain class on the inner wrapper itself, NOT gated by the
   outer .activity-related.expanded ancestor (2026-08-22, Mark: the very
   first expand of any movement -- the one that awaits the GET
   /betrans/.../bets fetch -- still snapped instantly instead of fading).
   An ancestor-selector rule resolves to its final value the instant a
   fresh child is inserted under an ALREADY-expanded parent (true for every
   content swap after the first one: Loading -> real content, or a
   collapse-then-reopen), with no earlier frame left to fade from -- see
   setRelatedContent()'s own comment in app.js, which forces a reflow
   between insertion and adding .visible so this rule change always has a
   real "before" frame to transition out of, regardless of the outer
   element's own state. */
.activity-related-inner { overflow: hidden; min-height: 0; opacity: 0; transition: opacity 0.2s ease 0.05s; }
.activity-related-inner.visible { opacity: 1; }
.activity-related .item { cursor: default; margin-bottom: 0.4rem; background: #333333; }
.activity-related .item:last-child { margin-bottom: 0; }
/* Celebratory glow, time-windowed (2026-08-17, Mark) -- see winnerGlowAt
   in app.js's loadBets(). GREEN, not yellow: the page background is
   already yellow, so a yellow glow on a black card barely registered
   against it (Mark: "no veo el efecto") -- green both pops against both
   the black card and the yellow page, AND matches the ✓ mark's own color.
   Flashes the card's actual background, not just a soft shadow around it,
   since a shadow alone read as too subtle to notice at a glance. */
@keyframes justWonGlow {
  0% { background: rgba(52,211,153,0.45); box-shadow: 0 0 22px 6px rgba(52,211,153,0.6); }
  100% { background: var(--card); box-shadow: 0 0 0 0 rgba(52,211,153,0); }
}
.item-just-won { animation: justWonGlow 1.8s ease; }
.item-chevron { display: inline-block; color: var(--muted); font-size: 0.7rem; transition: transform 0.15s ease; }
.item-expanded .item-chevron { transform: rotate(180deg); }
/* Collapsed by default, animated open/closed via the .expanded class
   (2026-08-22, Mark: "se puede animar tambien el despliegue y repliegue de
   los detalles") -- NOT the `hidden` attribute any more (this page's own
   global `[hidden]{display:none!important}` rule can't be transitioned).
   max-height is a generous fixed cap, not the real content height -- there
   is no CSS-only way to transition to an unknown "auto" height, and 600px
   comfortably covers even a many-leg system bet's own detail rows.
   border-top-width (not just color) animates too so the collapsed state
   has no lingering 1px line -- max-height:0 clips CONTENT, not the
   element's own border box. */
.item-detail {
  display: flex; flex-direction: column; gap: 0.3rem;
  margin-top: 0; padding-top: 0;
  border-top: 0 solid var(--border);
  max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height 0.25s ease, opacity 0.2s ease, margin-top 0.25s ease, padding-top 0.25s ease, border-top-width 0.25s ease;
}
.item-detail.expanded {
  margin-top: 0.5rem; padding-top: 0.5rem;
  border-top-width: 1px;
  max-height: 600px; opacity: 1;
}
.item-detail-row { display: flex; justify-content: space-between; gap: 0.5rem; font-size: 0.76rem; }
/* Selection's own market/match paused or ended live, while its "view
   ticket" detail happens to be open (2026-08-21, Mark) -- same dimmed
   treatment as the board's own .odd-btn:disabled. */
.item-detail-row.suspended { opacity: 0.45; }
/* Same one-shot tick flash as .pick-row (see pickFlashUp/Down above) --
   this row's background is the same --card as a pick-row's, unlike the
   board's own .odd-btn (--surface). */
.item-detail-row.flash-up { animation: pickFlashUp 0.6s ease; }
.item-detail-row.flash-down { animation: pickFlashDown 0.6s ease; }
/* Each selection's own event countdown, active bets only (2026-08-18,
   Mark: "al lado de cada seleccion ... el tiempo restante del evento") --
   same muted look as Offers' own .offer-countdown, just inline next to
   the selection's own name instead of a card corner. */
.item-detail-remaining { color: var(--muted); font-size: 0.7rem; }
/* Same collapsed-by-default/animated-via-.expanded treatment as
   .item-detail above, kept in sync with it by the same click handler
   (app.js) -- smaller max-height cap since this is always exactly one
   row, unlike .item-detail's variable-length selection list. */
.item-cashout-row {
  display: flex; align-items: center; gap: 0.5rem;
  margin-top: 0; max-height: 0; opacity: 0; overflow: hidden;
  transition: max-height 0.25s ease, opacity 0.2s ease, margin-top 0.25s ease;
}
.item-cashout-row.expanded { margin-top: 0.5rem; max-height: 90px; opacity: 1; }
/* Slider (2026-08-17, Mark: "el input del cashout hacelo como slide bar")
   -- gets the lion's share of the row's width since it's the one thing
   that actually needs room to drag; the value readout is fixed-width so
   digit changes don't jitter the layout, and the button only takes what
   its own label needs. */
.cashout-value { flex: 0 0 auto; min-width: 2.6rem; font-size: 0.78rem; color: var(--fg); font-variant-numeric: tabular-nums; }
.cashout-amount {
  flex: 3 1 auto; min-width: 0; margin: 0; height: 8px; border-radius: 999px;
  background: var(--surface); accent-color: var(--yellow); cursor: pointer;
  /* Opts back into native horizontal touch handling (2026-08-21) -- same
     reasoning as .odds-sports-grid's own touch-action override: .app sets
     touch-action:pan-y app-wide so an unconsumed horizontal drag never
     leaks into the browser's own edge-swipe gesture, but a REAL <input
     type=range> thumb needs horizontal touch too. wireTabSwipe() also
     excludes this element outright (belt-and-suspenders -- the JS-level
     exclusion is what actually stops the tab-swipe gesture from firing;
     this is what keeps the slider's own native drag working smoothly
     regardless). */
  touch-action: pan-x pan-y;
}
.cashout-amount::-webkit-slider-thumb { width: 20px; height: 20px; }
.cashout-amount::-moz-range-thumb { width: 20px; height: 20px; border: none; }
.item-cashout-row .btn-ghost {
  flex: 0 0 auto; width: auto; margin-top: 0; padding: 0.3rem 0.6rem; font-size: 0.7rem; white-space: nowrap;
}
.item-cashout-row .btn-ghost:disabled { opacity: 0.4; cursor: not-allowed; }
.pill {
  font-size: 0.66rem; font-weight: 800; padding: 0.1rem 0.5rem; border-radius: 999px;
  background: var(--surface); color: var(--fg); white-space: nowrap;
}
.credit { color: var(--yellow); font-weight: 700; }
.debit { color: var(--danger); font-weight: 700; }

/* ---------- ticket wizard (full-screen, covers the tab bar) ---------- */
.ticket-wizard { display: flex; flex-direction: column; padding: 0; background: var(--bg); }
/* Wizard's own header band is gone entirely (2026-08-17, Mark: "eliminar
   completamente la banda del titulo") -- Cancel/Place bet in the footer
   are the only way out now (see btnWizardCancel in app.js), the tab bar
   underneath stays hidden while this screen is open (same as before). */
.wizard-body { flex: 1 1 auto; overflow-y: auto; padding: 0.9rem 1.1rem; }
/* Cancel/Continue/Place-bet bar made to match #tabbar EXACTLY in position
   and size (2026-08-21, Mark: "tiene que tener la misma posicion y tamaño
   que la banda del menu inferior") -- both were flush-bottom/full-width
   already (background fills the padding box regardless of inset), the
   real gap was HEIGHT: this bar used to also carry `.wizard-error` inside
   it with a permanently-reserved `min-height`, making it ~94px tall vs.
   the tab bar's own emergent ~54px. `.wizard-error` moved OUT to its own
   sibling row (see index.html) so it only takes space while it actually
   has text (`:empty` below); `min-height` here is pinned to the SAME
   literal value as `#tabbar` gets (see that rule's own comment) so
   neither can silently drift apart from the other again. */
.wizard-footer {
  flex: 0 0 auto; min-height: 3.015rem; padding: 0 1.1rem;
  padding-bottom: env(safe-area-inset-bottom);
  display: flex; align-items: center;
  background: var(--card);
}
.wizard-actions { flex: 1 1 auto; }
.wizard-error { flex: 0 0 auto; color: var(--danger); font-size: 0.8rem; padding: 0.5rem 1.1rem 0; }
.wizard-error:empty { display: none; }

/* Accept-odds-changes select (2026-08-18, Mark: "el combo que ya
   teniamos q permite seleccionar ... si solo acepta cotizaciones mayores
   o iguales, o todos los cambios de cotizacion") -- same idea as
   betslip-widget.js's own header select, just two options and placed
   above the pick list here since this wizard has no header band of its
   own (see .ticket-wizard's own comment above). */
.odds-policy-row { margin-bottom: 0.7rem; }
.odds-policy-row select {
  width: 100%; padding: 0.5rem 0.6rem; border-radius: 10px; border: 1px solid var(--border);
  background: var(--surface); color: var(--fg); font-size: 0.8rem; font-weight: 700;
}

.pick-row {
  display: flex; justify-content: space-between; align-items: center; gap: 0.5rem;
  background: var(--card); border-radius: 10px;
  padding: 0.55rem 0.7rem; margin-bottom: 0.5rem; font-size: 0.85rem; color: var(--fg);
}
.pick-odd { color: var(--yellow); font-weight: 800; }
/* Same one-shot flash as Offers' own .odd-btn.flash-up/.flash-down
   (2026-08-18, Mark: "la misma logica de odometro, glow y flechas ...
   como se muestra en offers") -- own keyframes rather than reusing
   oddFlashUp/oddFlashDown as-is, since those end on --surface (the
   board's own nested-tappable background), while a pick-row's own
   background is --card. */
@keyframes pickFlashUp { from { background: rgba(52,211,153,0.4); } to { background: var(--card); } }
@keyframes pickFlashDown { from { background: rgba(226,96,122,0.4); } to { background: var(--card); } }
.pick-row.flash-up { animation: pickFlashUp 0.6s ease; }
.pick-row.flash-down { animation: pickFlashDown 0.6s ease; }
.remove-pick { background: rgba(226,96,122,0.16); color: var(--danger); border: none; border-radius: 999px; width: 1.6rem; height: 1.6rem; cursor: pointer; }
/* A pick whose event just finished fades out instead of vanishing
   (2026-08-17, Mark) -- same fade/collapse pattern as the Offers board's
   own .offer-card.leaving; see the settlement-ticker callback in app.js
   for the mark-leaving-then-splice-after-the-animation sequencing. */
@keyframes pickRowLeave {
  from { opacity: 1; max-height: 60px; margin-bottom: 0.5rem; }
  to { opacity: 0; max-height: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; }
}
.pick-row.leaving { animation: pickRowLeave 0.4s ease forwards; overflow: hidden; }
/* Staged pick whose match paused or ended while the ticket wizard was open
   (2026-08-21, Mark: "si el partido se pausa o termina, inhabilitarlo en
   la vista de view ticket") -- same dimmed treatment as .item-detail-row's
   own .suspended in My Bets; the remove (×) button stays enabled so a
   stale pick can still be taken off the ticket. */
.pick-row.suspended { opacity: 0.45; }

/* "+ Add another selection" -- ghost/outline style, matching the
   reference's "Log In" button (dark fill, yellow border+text). */
.btn-ghost {
  width: 100%; margin-top: 0.5rem; padding: 0.7rem; border-radius: 999px;
  background: var(--card); border: 1px solid var(--yellow); color: var(--yellow);
  font-weight: 700; font-size: 0.88rem; cursor: pointer;
}

/* Extra breathing room above the mode tabs (2026-08-17, Mark: "+ Add
   another selection" and Single/Combo/System looked too close together) --
   margin-top lives here rather than on .btn-ghost's own margin-bottom, so
   it doesn't also affect that class's OTHER use (the Cash out button in
   My Bets' cashout row, which resets margin-top but not a margin-bottom). */
.mode-tabs { display: flex; gap: 0.5rem; margin-top: 1.1rem; margin-bottom: 1rem; }
.mode-btn {
  flex: 1; padding: 0.55rem 0; border: 1px solid var(--border); background: var(--card); color: var(--muted);
  border-radius: 999px; font-weight: 700; font-size: 0.85rem; cursor: pointer;
}
.mode-btn.active { background: var(--yellow); border-color: var(--yellow); color: var(--ink); }
.mode-btn:disabled { opacity: 0.4; }

.size-chips { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.75rem; }
.size-chip {
  padding: 0.45rem 0.9rem; border-radius: 999px; border: 1px solid var(--border); background: var(--card);
  color: var(--fg); font-weight: 700; cursor: pointer;
}
.size-chip.active { background: var(--yellow); border-color: var(--yellow); color: var(--ink); }

/* Stake slider (2026-08-17, Mark: log scale, $1 min, starts at $5, up to
   the whole balance) -- same big prominent number + slider treatment the
   old plain number box had, just a drag control now instead of a keyboard
   one. */
/* Currency picker for the bet about to be placed (2026-08-21, Mark:
   "adoptar el mismo mecanismo al momento de hacer el place bet, permitir
   ahi cambiar la moneda... tambien con un swipe horizontal y agregar
   flechas laterales"; briefly merged onto the Place-bet button itself,
   REVERTED back to this standalone bar the same day -- Mark: "estaba mas
   linda tu version del selector de monedas... dejar una barra separada")
   -- same dark-card language as .stake-box right below it, same swipe
   gesture + .wallet-currency-nav arrows as the Wallet screen's own
   balance box, just a compact single-row label instead of a big number
   (see wireCurrencySwipe()/updateStakeCurrencyBox() in app.js).
   position:relative anchors the arrows, same as .wallet-balance-box's
   own. */
.stake-currency-box {
  background: var(--card); border-radius: 12px; padding: 0.6rem 2rem; margin-bottom: 0.6rem;
  text-align: center; position: relative;
  overflow: hidden; /* clips .stake-currency-card/-preview mid-slide, same as .wallet-balance-box's own */
}
/* Same card-swap slide as .wallet-balance-box's own (2026-08-21, Mark:
   "hacer el mismo comportamiento para la #stakecurrencybox") -- see that
   box's own comment. */
.stake-currency-card-preview { top: 0.6rem; left: 2rem; right: 2rem; }
.stake-currency-label { color: var(--muted); font-size: 0.85rem; }
.stake-currency-label b { color: var(--fg); font-weight: 800; }
.stake-box { background: var(--card); border-radius: 12px; padding: 0.85rem 0.9rem 1rem; margin-bottom: 0.9rem; }
.stake-value { font-size: 1.3rem; font-weight: 800; text-align: center; color: var(--fg); margin-bottom: 0.7rem; font-variant-numeric: tabular-nums; }
.stake-slider {
  display: block; width: 100%; margin: 0; height: 8px; border-radius: 999px;
  background: var(--surface); accent-color: var(--yellow); cursor: pointer;
}
.stake-slider::-webkit-slider-thumb { width: 22px; height: 22px; }
.stake-slider::-moz-range-thumb { width: 22px; height: 22px; border: none; }
.preview-line { display: flex; justify-content: space-between; font-size: 0.88rem; padding: 0.35rem 0; color: var(--muted); }
.preview-line b { color: var(--yellow); }
.preview-currency { opacity: 0.75; font-size: 0.92em; }
.preview-odds { opacity: 0.75; font-size: 0.92em; }

.confirm-totals { background: var(--card); border-radius: 10px; padding: 0.2rem 0.8rem; margin-top: 0.9rem; }

.btn-primary {
  width: 100%; padding: 0.85rem; border: none; border-radius: 999px;
  background: var(--yellow); color: var(--ink);
  font-weight: 800; font-size: 0.95rem; cursor: pointer;
}
.btn-primary:disabled { opacity: 0.5; }

/* Cancel/Place bet/x1000 share one row (2026-08-17, originally forced to
   equal thirds; restyled 2026-08-21, Mark: "reducir los botones Cancel y
   x1000 y aumentar el tamaño del boton central... muy pegados a los
   bordes superior e inferior de la banda negra"). Place bet -- the one
   button that actually matters -- is now the visually dominant CTA;
   Cancel/x1000 shrink to secondary, auto-sized actions on either side of
   it. `margin: 0.55rem 0` on all three (not reliant on the bar's own
   flex centering alone) is what actually pulls them off the black bar's
   top/bottom edge. */
.wizard-actions { display: flex; align-items: center; gap: 0.5rem; }
.wizard-actions > button { width: auto; margin: 0.32rem 0; padding: 0.76rem; cursor: pointer; }
#btnWizardCancel, #btnPlaceBetX1000 { flex: 0 0 auto; padding: 0.5rem 0.85rem; font-size: 0.76rem; }
#btnWizardNext { flex: 1 1 auto; padding: 0.65rem 0.5rem; font-size: 0.96rem; }

.hint { color: var(--ink); opacity: 0.75; font-size: 0.85rem; }
/* A genuinely-empty list/screen, not an error (2026-08-21, Mark: "los
   estados vacios son muy secos... una frase mas amable... ayudaria" --
   icon dropped same day, "sacar los iconos... chau dado, finish flag,
   mailbox y similares") -- same ink-on-yellow color as .hint, just given
   room to breathe instead of reading like one more line of dense error
   text. See emptyStateHtml() in app.js. */
.hint-empty { padding: 1.6rem 1.2rem; text-align: center; opacity: 0.85; }

/* Skeleton loading placeholders (2026-08-20) -- roughly the same box
   shape/spacing as .odds-match-card so content doesn't visibly jump when
   the real cards replace these. Shimmer disabled under
   prefers-reduced-motion, leaving a plain static gray placeholder. */
.odds-skeleton-card {
  background: var(--card); border-radius: 14px;
  padding: 0.8rem 0.9rem; margin-bottom: 0.6rem;
}
.odds-skeleton-card--row { padding: 0.5rem 0.7rem; margin-bottom: 0; }
.odds-skeleton-line {
  height: 0.75rem; border-radius: 5px; margin-bottom: 0.5rem;
  background: linear-gradient(90deg, var(--surface) 25%, var(--border) 37%, var(--surface) 63%);
  background-size: 400% 100%;
  animation: oddsSkeletonShimmer 1.4s ease infinite;
}
.odds-skeleton-line:last-child { margin-bottom: 0; }
.odds-skeleton-line--narrow { width: 40%; }
.odds-skeleton-line--wide { width: 75%; height: 0.95rem; }
@keyframes oddsSkeletonShimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}
@media (prefers-reduced-motion: reduce) {
  .odds-skeleton-line { animation: none; background: var(--surface); }
}
/* .wizard-body's own hints (2026-08-17, Mark: "oscurecer un poco el gris
   q se pierde con el fondo") sit directly on the yellow page background,
   same as .screen-title used to -- muted gray had barely any contrast
   there, so this now falls back to the base ink treatment above. Only
   .flat-list (My Bets/Activity) still uses muted -- its hints ("No bets
   yet.") sit in the same spot but that pairing hasn't been flagged. */
.flat-list .hint { color: var(--muted); opacity: 1; }
/* Shown once ever (2026-08-17, Mark) -- see loadBets()'s own
   BETS_HINT_SEEN_KEY check. */
.bets-hint { text-align: center; margin: 0 0 0.6rem; }

/* ---------- floating "view ticket" + tab bar ---------- */
.fab {
  /* Lowered (2026-08-20, Mark: "bajar el boton del carrito 'view tickets'
     un poco para que no tape la ultima oferta") -- 4.6rem left ~20px of
     clearance above the tab bar, plenty of room to drop it further
     without actually overlapping the tab bar itself. */
  position: absolute; left: 1.1rem; right: 1.1rem; bottom: calc(3.5rem + env(safe-area-inset-bottom));
  z-index: 5; padding: 0.7rem; border: none; border-radius: 999px;
  background: var(--card); color: var(--yellow); border: 1px solid var(--yellow); font-weight: 800; font-size: 0.9rem; cursor: pointer;
  box-shadow: 0 8px 20px rgba(0,0,0,0.35);
}

.tabbar {
  flex: 0 0 auto; display: flex; background: var(--card);
  padding-bottom: env(safe-area-inset-bottom);
  /* Pinned to a literal value, not left purely emergent from .tab's own
     padding/icon/text -- the ticket wizard's own Cancel/Place-bet bar
     (`.wizard-footer`) is pinned to the SAME value so the two bars can
     never silently drift apart again (2026-08-21, Mark asked for them to
     match exactly). Was 3.35rem (matched what .tab's own content
     rendered to); lowered 10% to 3.015rem 2026-08-23 (Mark: "q qeden de
     la misma altura ... todas del mismo alto un 10% menor al actual") --
     update BOTH this and .wizard-footer's own min-height together, same
     as any future change to either. */
  min-height: 3.015rem;
}
.tab {
  flex: 1; display: flex; flex-direction: column; align-items: center; gap: 0.2rem;
  padding: 0.4rem 0; background: transparent; border: none; color: var(--muted); font-size: 0.7rem; cursor: pointer;
}
/* Monochrome outline icons (2026-08-17, Mark: only yellow/black/gray/white
   across the app) -- currentColor means they need no color of their own,
   just inherit .tab's existing gray/yellow states below. */
.tab-icon { display: flex; position: relative; }
.tab-icon svg { width: 1.3rem; height: 1.3rem; }
.tab.active { color: var(--yellow); }
.tab.active .tab-badge { background: var(--yellow); }

/* My Bets tab: unread-mail-style badge for the active bet count, plus a
   glow pulse when a bet wins (2026-08-17, Mark) -- both visible from ANY
   screen, not just while My Bets itself is open, since the row-level glow
   (see .item-just-won) is easy to miss if you're not looking at that exact
   spot at that exact moment. */
.tab-badge {
  position: absolute; top: -0.3rem; right: -0.55rem;
  min-width: 1.05rem; height: 1.05rem; padding: 0 0.25rem; border-radius: 999px;
  background: var(--muted); color: var(--ink); font-size: 0.6rem; font-weight: 800;
  display: flex; align-items: center; justify-content: center; line-height: 1;
}
@keyframes tabIconGlow {
  0% { filter: drop-shadow(0 0 0 rgba(52,211,153,0)); transform: scale(1); }
  35% { filter: drop-shadow(0 0 5px rgba(52,211,153,0.95)); transform: scale(1.3); }
  100% { filter: drop-shadow(0 0 0 rgba(52,211,153,0)); transform: scale(1); }
}
.tab-icon-glow svg { animation: tabIconGlow 1.1s ease; }

[hidden] { display: none !important; }

/* ---------- toasts ---------- */
/* Sits just below the appbar, not on top of it (2026-08-17, Mark: a toast
   was overlapping the balance odometer) -- 3.7rem clears the appbar's own
   measured ~56px height with a small margin; env() is additive on top of
   that same baseline, matching how the appbar's own padding-top grows for
   notched devices. */
.toast-stack { position: fixed; top: calc(3.7rem + env(safe-area-inset-top)); left: 0; right: 0; display: flex; flex-direction: column; align-items: center; gap: 0.5rem; pointer-events: none; z-index: 20; }

/* Blocking-error popup (2026-08-21, Mark: "capturar los errores y
   mostrarlo en formato humano en un emergente... x ejemplo el de fondos
   insuficientes") -- a real, dismiss-required modal for errors that stop
   an action cold (insufficient funds, account not found, ...), as opposed
   to the toast above, which is for a passing status update no one needs
   to actively acknowledge. See humanizeApiError()/showErrorModal() in
   app.js for what actually populates it. */
.error-modal-backdrop {
  position: fixed; inset: 0; background: rgba(0, 0, 0, 0.6);
  display: flex; align-items: center; justify-content: center;
  z-index: 50; padding: 1.5rem;
  animation: errorModalFadeIn 0.15s ease;
}
.error-modal-backdrop[hidden] { display: none; }
.error-modal {
  background: var(--card); border-radius: 16px; padding: 1.7rem 1.4rem 1.3rem;
  max-width: 320px; width: 100%; text-align: center;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  animation: errorModalPop 0.2s cubic-bezier(.22, .61, .36, 1);
}
.error-modal-icon { font-size: 2.1rem; margin-bottom: 0.5rem; }
.error-modal-title { font-size: 1.05rem; font-weight: 800; color: var(--fg); margin-bottom: 0.5rem; }
.error-modal-message { font-size: 0.9rem; color: var(--muted); margin-bottom: 1.3rem; line-height: 1.45; }
.error-modal button { width: 100%; }
@keyframes errorModalFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes errorModalPop { from { opacity: 0; transform: scale(0.92) translateY(8px); } to { opacity: 1; transform: scale(1) translateY(0); } }
@media (prefers-reduced-motion: reduce) {
  .error-modal-backdrop, .error-modal { animation: none; }
}
.toast {
  background: var(--card); border-left: 3px solid var(--yellow);
  color: var(--fg);
  border-radius: 10px; padding: 0.5rem 0.9rem; font-size: 0.82rem; box-shadow: 0 6px 18px rgba(0,0,0,0.4);
  animation: toastEnter 0.25s ease;
}
.toast.toast-error { border-left-color: var(--danger); }
@keyframes toastEnter { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } }

/* Breadcrumb ("Sports / Tennis") and the status filter now share ONE row
   (2026-08-20, Mark: "los tres botones... en el mismo renglon q Sport/
   Tennis, justificado a la derecha") -- breadcrumb text on the left,
   filter pills pinned to the right via justify-content:space-between.
   .odds-breadcrumb itself no longer needs its own margin-bottom (the ROW
   carries it instead), and min-width:0 lets a long breadcrumb path
   truncate rather than push the filter off to the side. */
.odds-breadcrumb-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.6rem; }
.odds-breadcrumb { min-width: 0; padding: 0; font-size: 0.92rem; color: var(--ink); opacity: 0.85; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.odds-crumb { cursor: pointer; font-weight: 700; }
.odds-crumb:hover { text-decoration: underline; }
.odds-search-input { flex: 1 1 auto; min-width: 0; padding: 0.5rem 0.7rem; border-radius: 10px; border: 1px solid var(--border); background: var(--surface); color: var(--fg); font-size: 0.85rem; }
.odds-search-input--header { font-size: 0.82rem; padding: 0.45rem 0.65rem; }
.odds-sep { margin: 0 0.35rem; opacity: 0.6; }

/* Prematch/Live/All filter (2026-08-20, Mark: "un filtro para que solo me
   muestre prematch/live/all... que no ocupe demasiado lugar y siga con la
   misma estetica... one filter for all, sport and features") -- ONE
   shared filter, visible in the pinned header on every Offers view
   (Featured and any sport's own match list alike). Total width pinned to
   exactly ONE sport-card's own width (2026-08-20, Mark: "q ocupen en
   total el mismo ancho que una caja de deporte") -- now that
   .odds-sports-grid is column-major (grid-auto-columns:
   `(100% - 0.6rem) / 2`, see its own comment), referencing that SAME
   calc here again (as it originally did back when the grid was a plain
   repeat(2, 1fr)) keeps this in sync automatically. Each button is an
   equal 1/3 share of that fixed total (`flex:1 1 0`, not auto-sized to
   its own text -- that previously made Prematch/Live/All three different
   widths). */
.odds-status-filter { display: flex; flex: 0 0 auto; width: calc((100% - 0.6rem) / 2); gap: 0.25rem; }
/* Same dark-fill-when-active convention as .subtab (Active/Finished),
   NOT .mode-btn/.size-chip's yellow-fill-when-active -- this filter sits
   directly on the page's own yellow background, where a yellow-filled
   active pill (tried first) all but disappeared against it. .subtab
   lives on that same yellow background for the same reason. */
.odds-filter-btn {
  flex: 1 1 0; padding: 0.2rem 0; border: 1px solid var(--ink); background: transparent; color: var(--ink);
  opacity: 0.65; border-radius: 999px; font-weight: 700; font-size: 0.62rem; cursor: pointer;
}
.odds-filter-btn.active { background: var(--ink); color: var(--yellow); opacity: 1; }

/* Flush against #offersList's own top edge (2026-08-23, Mark: "subi el
   titulo Featured hasta que quede pegado en el topo de la lista
   scrolleable") -- no top margin left at all; #offersList itself carries
   no top padding of its own (only a bottom one, for FAB clearance), so
   removing this is the only thing between the title and the scroll
   container's real top. The match cards right below it in the DOM shift
   up by the exact same amount as a side effect of one less margin in the
   flow -- no separate change needed to move them ("desplaza los matches
   ... la misma cantidad"). */
.odds-section-title { font-size: 0.92rem; font-weight: 700; color: var(--ink); opacity: 0.85; margin: 0 0 0.6rem; }

/* margin-bottom (not padding on the pinned wrapper itself, which would
   also add unwanted space on drill-down/search screens where the wrapper
   sits empty) keeps a small gap so Featured's cards never look glued to
   the pinned sport buttons once scrolled up underneath them (2026-08-20,
   Mark: "dejar un pequeño margen entre los botones de deporte y el
   scroll"). */
/* Back to looking like the original 2-column/4-row grid (2026-08-20,
   Mark: "volvamos a este panel de 8, asi" + a screenshot of the old
   layout) -- but the whole thing now scrolls sideways ONE COLUMN at a
   time ("integramente debe poder desplazarse hacia los costados de a una
   columna") to reach sports beyond the visible 8, rather than either the
   old fixed 2-column grid (only 8 ever reachable) or the in-between
   single-ROW horizontally-scrolling strip tried right before this (Mark:
   "no me gusta"). A CSS grid with grid-auto-flow:column and a FIXED
   row count (4) is what makes this "column-major" -- item 1 (Football)
   and item 2 (Basketball) still land in row 1 side by side (matching the
   screenshot) because grid-auto-columns sizes each column to exactly
   HALF the visible strip width (`(100% - gap) / 2`), so items 1-8 fill
   out 2 full columns (= the original 8-card page) before a 3rd column
   (items 9-12) even starts -- that 3rd column is what scrolling right
   reveals. scroll-snap-type:x MANDATORY (not proximity, used on the
   single-row attempt) + scroll-snap-align on each card is what gives the
   deliberate "one column" paginated feel instead of free scrolling.
   /v1/sports/all (LEFT-JOIN based, includes 0-match sports) is still the
   data source underneath all of this -- unchanged from the previous
   attempt, only the layout changed. Still no visible scrollbar. */
.odds-sports-grid {
  display: grid; grid-template-rows: repeat(4, auto); grid-auto-flow: column;
  grid-auto-columns: calc((100% - 0.6rem) / 2);
  gap: 0.6rem; margin-bottom: 0.6rem;
  overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
  scrollbar-width: none; -ms-overflow-style: none;
  /* Opt back into native horizontal panning (see .app's touch-action:pan-y
     comment above -- everything else in the app has it disabled) and stop
     a drag PAST this grid's own left/right scroll limit from chaining out
     to the browser's edge-swipe gesture once it's exhausted, same
     overscroll-behavior-y:contain idea as the pull-to-refresh fix, just
     the horizontal axis. */
  touch-action: pan-x pan-y;
  overscroll-behavior-x: contain;
}
.odds-sports-grid::-webkit-scrollbar { display: none; }
/* Small black "you can scroll sideways" triangle hints at each edge --
   REAL <button>s now (2026-08-20, Mark: "que los triangulos sean
   clickeables y avancen de a una columna y si llegan al final o esta al
   principio... q se inhabiliten"), not the CSS-pseudo-element-only
   version tried first (kept the exact same visual triangle -- border
   trick on the button's own ::before -- and the exact same negative
   offset into the yellow gutter, since that part already worked and
   looked right). :disabled just fades it out; app.js
   (updateOddsSportsNav) toggles that based on actual scroll position. */
#oddsSportsGrid { position: relative; }
.odds-sports-nav {
  /* margin-top nudged from -10px (dead center of the whole 4-row panel)
     to -14.8px (2026-08-20, Mark: "center the triangles to the gap
     between second and third button") -- lines the triangle's own
     center up with the seam between row 2 and row 3 instead of the
     panel's raw vertical midpoint; measured directly against real card
     boundingClientRects rather than eyeballed. */
  position: absolute; top: 50%; margin-top: -14.8px; width: 20px; height: 20px;
  padding: 0; border: none; background: transparent; cursor: pointer; z-index: 3;
  display: flex; align-items: center; justify-content: center;
}
.odds-sports-nav::before {
  content: ""; width: 0; height: 0; border-top: 6px solid transparent; border-bottom: 6px solid transparent;
}
.odds-sports-nav-left { left: -0.65rem; }
.odds-sports-nav-left::before { border-right: 7px solid #000; }
.odds-sports-nav-right { right: -0.65rem; }
.odds-sports-nav-right::before { border-left: 7px solid #000; }
.odds-sports-nav:disabled { opacity: 0.25; cursor: default; }
/* Pinned at the top of the Offers screen while Featured scrolls underneath
   (2026-08-20, Mark: "el scroll de featured no desplaza hacia arriba los
   botones de deportes"; then, after position:sticky turned out to NOT
   actually pin it in practice: "aun se mueven los botones de deporte, q
   tenga el mismo comportamiento que balance, fijo arriba"). #oddsSportsGrid
   is its own dedicated element (see renderSportsGrid() in app.js),
   sibling to #offersList, so #screen-offers can use the SAME
   fixed-header/scrolling-body flex split already proven on
   #screen-wallet/#screen-bets, rather than sticky positioning. Drill-down/
   search views never populate #oddsSportsGrid (render() clears it on
   every other navigation) so they're unaffected -- #offersList alone
   still scrolls normally for them, same as before. */
#screen-offers { display: flex; flex-direction: column; overflow-y: hidden; }
#screen-offers > .odds-breadcrumb-row,
#screen-offers > #oddsSportsGrid { flex: 0 0 auto; }
/* scrollbar-gutter:stable (2026-08-20, Mark: "que las sports cards...
   no cambien de ancho por la aparicion o no de la barra de scroll") --
   .screen's own copy of this same rule stopped applying once
   #screen-offers itself stopped being the scrolling element (this flex
   restructuring made #offersList the real scroller instead) -- without
   it, cards visibly narrowed the instant the list grew tall enough to
   need a scrollbar (Featured, or a sport with many matches). */
#screen-offers > #offersList { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; scrollbar-gutter: stable; min-height: 0; }
/* Extra bottom scroll room while the FAB is showing (2026-08-20, Mark:
   "bajar el boton del carrito... para que no tape la ultima oferta") --
   .fab itself is `position:absolute`, so it always covers the same
   screen position no matter how far #offersList has scrolled; only
   giving the LIST room to scroll further (past where its content would
   otherwise end) lets the last card actually clear it. Toggled by
   updateFab() in app.js, only while a pick is staged. */
#offersList.fab-clearance { padding-bottom: 4.5rem; }
/* Same icon-left/two-stacked-lines shape as oddsfeed's own .sport-card
   (2026-08-20, Mark: "replicar el diseño de los botones de oddsfeed") --
   shorter than the old icon-on-top layout since the icon no longer
   forces its own line above the text. */
.odds-sport-card {
  background: var(--card); border-radius: 14px; padding: 0.55rem 0.7rem; color: var(--fg); cursor: pointer;
  display: flex; align-items: center; gap: 0.55rem; min-width: 0;
  scroll-snap-align: start;
  /* Caps a nav button's large scrollBy() request (2026-08-20) to just
     ONE column per click, instead of the browser skipping past several
     snap points in one smooth-scroll motion -- lets app.js request an
     oversized scroll distance without needing to compute the exact
     column+gap width by hand. */
  scroll-snap-stop: always;
}
.odds-sport-icon { width: 24px; height: 24px; flex-shrink: 0; filter: invert(1); opacity: 0.9; }
.odds-sport-body { min-width: 0; }
.odds-sport-name { font-weight: 700; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.odds-sport-meta { color: var(--muted); font-size: 0.72rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Match card -- same stacked-lines/corner-badge shape oddsfeed's own
   .match-card settled on (team names never share a line with the
   country/tournament label, corners reserved for status/market-count so
   they never eat into the text column). */
.odds-match-card {
  position: relative;
  background: var(--card); border-radius: 14px;
  padding: 0.8rem 0.9rem; margin-bottom: 0.6rem; color: var(--fg);
  display: flex; justify-content: space-between; align-items: center; gap: 0.7rem; flex-wrap: nowrap;
  overflow: hidden;
}
@keyframes matchCardLeave {
  from { opacity: 1; max-height: 260px; margin-bottom: 0.6rem; }
  to { opacity: 0; max-height: 0; margin-bottom: 0; padding-top: 0; padding-bottom: 0; }
}
.odds-match-card.leaving, .odds-expand.leaving { animation: matchCardLeave 0.4s ease forwards; overflow: hidden; }
/* Sport icon as a subtle background watermark (2026-08-20, Mark: "los
   iconos de deporte... de fondo en la tarjeta de partido como marca de
   agua"; then "deberia quedar en el centro de la tarjeta y ser un 20%
   mas grande" -- centered via top/left 50% + translate, and 4.6rem ->
   5.52rem, exactly +20%). Painted first in the DOM (before every other
   child), so plain DOM-order stacking already puts it behind everything
   else -- no z-index needed. filter:invert(1) turns the source
   pictograms (black silhouettes on transparent, per oddsfeed's own
   Wikimedia-sourced icon_svg) into a light mark visible against this
   card's dark background; low opacity keeps it decorative, never
   competing with the real text/prices on top. overflow:hidden on the
   card (above) still guards against it ever exceeding the card's own
   rounded corners at this larger size. */
.odds-card-watermark {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 5.52rem; height: 5.52rem;
  opacity: 0.10; filter: invert(1); pointer-events: none;
}
.odds-match-main { min-width: 0; flex: 1 1 auto; cursor: pointer; }
.odds-match-main > *:first-child { margin-top: 0.9rem; }
.odds-corner-left, .odds-corner-right {
  position: absolute; font-size: 0.68rem; font-weight: 800;
  letter-spacing: 0.03em; color: var(--muted);
}
/* 3px higher (2026-08-20, Mark: "quedo muy pegado a winner") -- more
   clearance from the market label ("1X2"/"WINNER") sitting just below it. */
.odds-corner-right { top: calc(0.35rem - 3px); right: 0.9rem; text-transform: uppercase; }
/* Green (2026-08-20, Mark: "5 MARKETS, en verde") -- var(--up), the same
   green already used for live status/up-ticks elsewhere in this app.
   Bottom-right, not top-left (2026-08-21, Mark: "colocar este label lo
   mas abajo a la derecha posible sin aumentar la altura de la tarjeta")
   -- the card's own primary-odds-row sits vertically centered, leaving
   just enough empty gutter below it (confirmed via a real measured
   card: ~7px past the row's own bottom edge, on top of the card's
   existing bottom padding) for this label to sit there without pushing
   the card any taller. text-transform: capitalize (not uppercase, see
   the shared rule above) is the OTHER half of the same request -- "5
   Markets", not "5 MARKETS". */
/* Centered horizontally, same row as before (2026-08-23, Mark: "ponele en
   el mismo renglon pero centrado horizontalente en la tarjeta") -- was
   right: 0.9rem (right-aligned); left+right: 0 with text-align: center
   spans the card's own width instead, keeping the same `bottom` (same
   vertical row) untouched. */
.odds-corner-left { bottom: 0.35rem; left: 0; right: 0; text-align: center; color: var(--up); text-transform: capitalize; cursor: pointer; }
/* Small down-arrow next to the counter, same green, indicating the card
   expands for more markets (2026-08-23, Mark: "una pequeña flecha tambien
   verde q indique q se puede deplegar hacia abajo") -- same "▾" glyph
   already used for this exact affordance elsewhere (.item-chevron), just
   without that one's rotate-on-expand animation: this whole label
   disappears on expand instead (see .odds-card-expanded below), so there's
   no "open" state left for it to rotate into. */
.odds-expand-arrow { display: inline-block; margin-left: 0.15em; }
/* Hides the "N markets ▾" counter entirely while its panel is open, shown
   again only on collapse (2026-08-23, Mark: "una vez desplegado, ocultar
   el contador de markets, solo volver a mostrarlo cuando colapse") -- see
   wireOddsMatchCards() in app.js for where this class is toggled. */
.odds-match-card.odds-card-expanded .odds-corner-left { display: none; }
.odds-status-badge { font-size: 0.6rem; font-weight: 400; }
.odds-status-badge.odds-status-live { color: var(--up); }
.odds-status-badge.odds-status-ended { color: var(--danger); }
.odds-status-badge.odds-status-paused { color: #e0a83f; }
/* Live countdown clock (2026-08-20, Mark: "el reloj de live no ocupe un
   renglon mas... Live (icono reloj) mm:ss... siempre verde, rojo en los
   ultimos 10s") -- INLINE inside .odds-status-badge.odds-status-live
   itself (see oddsMatchStatusBadge() in app.js), never a second element,
   so a live card's height never grows. .odds-status-live already renders
   var(--up) (green) above -- that alone satisfies "green while live";
   this .critical override (added by the shared ticking interval in the
   final 10s) is the only thing that needs to exist here, turning the
   WHOLE "live ⏱ mm:ss" string red -- same --danger color already used by
   this file's own .offer-countdown.critical (the active-bet remaining-
   time display), so a red countdown reads the same everywhere it appears
   in this app. */
.odds-status-badge.odds-status-live.critical { color: var(--danger); }
.odds-competition {
  font-size: 0.68rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 0.2rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.odds-team { font-weight: 700; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Home/away numbered 1/2 to the left of each team name (2026-08-20, Mark:
   "numerar los equipos/participantes con 1, 2 a la izquierda") -- matches
   the same '1'/'2' outcome codes already used in the 1X2/winner price
   row just below, so the numbering is legible as "this row = outcome 1". */
.odds-team-num { display: inline-block; min-width: 1.1em; margin-right: 0.35em; color: var(--muted); font-weight: 800; }
.odds-meta-row { color: var(--muted); font-size: 0.72rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.odds-market-label { font-size: 0.65rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 0.3rem; }
.odds-row { display: flex; flex-wrap: wrap; gap: 0.4rem; }
/* flex:0 0 auto (not flex:1, no growing) so a market group's own row of
   odds boxes never stretches to fill the FULL expand-panel width -- that
   was the actual cause of the expand panel's boxes rendering visibly
   bigger than the primary row's (2026-08-20, Mark: "mantener el tamaño de
   la caja de odds del mismo tamaño q en el principal de 1x2") -- each
   group now shrink-wraps to its own natural content width instead, which
   also lets .odds-expand (below) sit two of them side by side on one row
   when they fit, matching oddsfeed's own expand-panel layout ("como en la
   demo anterior"). */
.odds-market-row { margin-bottom: 0; flex: 0 0 auto; }
.odds-market-row .odd-btn { flex: 0 0 auto; min-width: 46px; padding: 0.4rem 0.4rem; font-size: 0.72rem; }
.odds-market-row .odd-btn b { font-size: 0.8rem; }

/* Sibling of .odds-match-card, appended right after it -- the "every
   other market" panel, lazy-loaded on first expand (see
   wireOddsMatchCards() in app.js). flex-wrap here (not a plain block
   stack) is what lets two .odds-market-row groups share a row when both
   fit, same idiom as oddsfeed's own .match-odds-expand. */
.odds-expand {
  margin: -0.4rem 0 0.6rem; padding: 0.7rem 0.9rem; background: var(--card);
  border-top: 1px solid var(--border); border-radius: 0 0 14px 14px;
  display: flex; flex-wrap: wrap; gap: 0.8rem;
}
.odds-expand[hidden] { display: none; }

.odd-btn.suspended { opacity: 0.4; pointer-events: none; }

/* ---------- wallet ---------- */
.wallet-balance-box {
  /* margin-bottom matches .flat-list .item's own 0.6rem (2026-08-20, Mark:
     "pegarla un poco mas con la de balance, equidistante como esta la
     primera linea de activity") -- balance -> action box -> first activity
     card now sit the SAME distance apart throughout, instead of the
     balance/action gaps being wider than activity's own item spacing. */
  background: var(--card); border-radius: 14px; padding: 1.1rem 1rem; text-align: center; margin-bottom: 0.6rem;
  position: relative; /* anchors .wallet-currency-nav below */
  overflow: hidden; /* clips .wallet-balance-card/-preview mid-slide (see wireCurrencySwipe) */
}
/* The swipeable content itself, as its own element (2026-08-21, Mark:
   "hacer como si una nueva tarjeta ingresara por los costados, no que se
   actualice la misma tarjeta", extended same day to the wizard's own
   .stake-currency-box below -- "hacer el mismo comportamiento") --
   previously the label/value/dots were bare children of the box itself,
   updated in place with no transition. .currency-swipe-card is the shared
   marker wireCurrencySwipe() looks up (`.wallet-balance-card`/
   `.stake-currency-card` carry it ALONGSIDE their own specific class, for
   their own specific layout/spacing) -- lets ONE swipe implementation
   slide either box's real content off to one side while a
   .currency-swipe-card-preview (a throwaway clone built from the target
   currency, see walletBalanceCardHtml()/stakeCurrencyCardHtml()) slides
   in from the other, same card-swap language as the tab bar's own
   screen-to-screen swipe, just scoped to one small box instead of a whole
   screen. The nav arrows stay OUTSIDE this wrapper (siblings, not
   children) so they never slide with it. */
.currency-swipe-card-preview { position: absolute; }
/* left/right (not width) so the absolutely-positioned preview's own
   content area exactly matches the real card's -- each box's own padding
   is its offset parent's padding edge, which an absolute child's
   top/left/right are relative to, NOT its content edge -- so these
   mirror .wallet-balance-box's/.stake-currency-box's own padding exactly
   (1.1rem 1rem and 0.6rem 2rem respectively). */
.wallet-balance-card-preview { top: 1.1rem; left: 1rem; right: 1rem; }
/* Same clickable triangle as .odds-sports-nav (2026-08-21, Mark: "poner
   las mismas dos flechitas negras... asi se pueda tambien cambiar de
   moneda del wallet con el mouse") -- identical shape/border-trick, just
   centered on this box's own single-block height instead of that grid's
   row-seam math (nothing to align to here). Click handlers move
   currentCurrencyId by exactly one step, same non-circular bounds as the
   swipe gesture -- :disabled at either end reuses .odds-sports-nav's own
   fade, see renderWalletBalance() for the toggle. */
.wallet-currency-nav {
  position: absolute; top: 50%; transform: translateY(-50%); width: 20px; height: 20px;
  padding: 0; border: none; background: transparent; cursor: pointer; z-index: 3;
  display: flex; align-items: center; justify-content: center;
}
.wallet-currency-nav::before {
  content: ""; width: 0; height: 0; border-top: 6px solid transparent; border-bottom: 6px solid transparent;
}
.wallet-currency-nav-left { left: 0.2rem; }
.wallet-currency-nav-left::before { border-right: 7px solid var(--fg); }
.wallet-currency-nav-right { right: 0.2rem; }
.wallet-currency-nav-right::before { border-left: 7px solid var(--fg); }
.wallet-currency-nav:disabled { opacity: 0.25; cursor: default; }
.wallet-balance-label { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.3rem; }
/* Currency code next to the title, and the swipe-to-switch dots underneath
   (2026-08-21, Mark: "poner al costado del titulo la denominacion de la
   moneda... permitir con un scroll horizontal cambiar de moneda"). Normal
   weight/spacing, not uppercase-forced like the label itself -- reads as
   a distinct little tag, not a continuation of the same all-caps run. */
.wallet-balance-currency { text-transform: none; letter-spacing: normal; opacity: 0.85; }
.wallet-balance-value {
  font-weight: 800; font-size: 1.8rem; color: var(--fg);
  display: inline-flex; align-items: baseline; justify-content: center; font-variant-numeric: tabular-nums;
}
/* Non-withdrawable/freeplay breakdown, below the main (money_id=1) amount
   (2026-08-23, Mark: "debajo del monto de money_id=1 extraible... el
   monto del money_id=2 y money_id=3... mas pequeños para q entren los dos
   montos con sus titulos arriba en una sola franja") -- one row, two
   columns, each with its own small caption above a smaller-than-main
   amount; sized well under .wallet-balance-value so both fit side by side
   without wrapping at typical mobile widths. */
.wallet-balance-substrip { display: flex; justify-content: center; gap: 1.4rem; margin-top: 0.5rem; }
.wallet-balance-sub-label { color: var(--muted); font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.15rem; }
.wallet-balance-sub-value { font-weight: 700; font-size: 0.88rem; color: var(--fg); font-variant-numeric: tabular-nums; }
.wallet-currency-dots { display: flex; justify-content: center; gap: 0.3rem; margin-top: 0.55rem; }
.wallet-currency-dot { width: 0.4rem; height: 0.4rem; border-radius: 50%; background: var(--border); }
.wallet-currency-dot.active { background: var(--yellow); }
.wallet-error { color: var(--danger); font-size: 0.82rem; min-height: 1.1rem; margin-bottom: 0.4rem; }
/* Collapses to nothing while there's no error (2026-08-20, same "pegarla
   un poco mas... equidistante" request) -- otherwise its own reserved
   anti-jank height sat between the balance box and the action box
   EVERY time, making that gap much wider than the 0.6rem used everywhere
   else, even though it's empty almost always. Still reserves its normal
   height the instant an error actually appears (textContent no longer
   empty), so nothing jumps around when that happens. */
.wallet-error:empty { min-height: 0; margin-bottom: 0; }
/* Smaller now that the "Amount" label is gone (2026-08-20, Mark: "sacar
   el titulo 'amount' de wallet y reducir la caja") -- less padding since
   the box only ever holds one row (input + two buttons) now. */
.wallet-action { background: var(--card); border-radius: 14px; padding: 0.55rem 0.9rem; margin-bottom: 0.6rem; }
.wallet-action-row { display: flex; gap: 0.5rem; align-items: stretch; }
/* Explicit height on input AND both buttons (2026-08-20, Mark: "boton de
   withdraw del mismo alto que el box para el monto") -- matching
   padding/font-size alone (tried first) still left Withdraw ~8px shorter
   than the input in practice; an explicit shared height sidesteps
   whatever box-model quirk caused that (a bare number input's own
   intrinsic height doesn't reduce to simple padding+line-height math the
   same way a button's does) instead of chasing it further. */
.wallet-action-row input {
  height: 2.86rem; flex: 1 1 auto; min-width: 0; padding: 0 0.7rem; border-radius: 10px;
  border: 1px solid var(--border); background: var(--surface); color: var(--fg); font-size: 0.95rem;
  box-sizing: border-box;
}
/* width:auto overrides .btn-primary/.btn-ghost's own width:100% -- same
   fix .wizard-actions > button already needed for the same reason
   (flex-basis:auto defers to an explicit width property when set, so
   flex:0 0 auto alone doesn't stop it from stretching full-width). */
/* Explicit shared min-width (2026-08-20, Mark: "hacer botones de withdraw
   y deposit del mismo tamaño") -- both are width:auto/flex:0 0 auto, so
   without this each just sized to its own text ("Withdraw" being longer
   than "Deposit" made them visibly different widths). font-size/line-height
   forced to match the input exactly too (2026-08-20, "boton de withdraw
   del mismo alto que el box para el monto") -- .btn-ghost's own base
   font-size (0.88rem) is smaller than the input's (0.95rem), which alone
   made Withdraw render a hair shorter than the amount box despite
   identical padding. */
.wallet-action-row button {
  /* margin:0 overrides .btn-ghost's own margin-top:0.5rem (used elsewhere
     for spacing above "+ Add another selection") -- without resetting it
     here, Withdraw (a .btn-ghost) sat visibly lower than Deposit despite
     matching heights (2026-08-20, Mark: "withdraw desalineado"). */
  height: 2.86rem; width: auto; margin: 0; flex: 0 0 auto; min-width: 6.2rem; padding: 0 1.1rem; border-radius: 10px;
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 0.95rem; box-sizing: border-box; cursor: pointer;
}

/* Balance/deposit/withdraw fixed at top, only the activity list below them
   scrolls (2026-08-20, Mark: "el scroll en las activities de wallet, solo
   para las activities, dejar siempre arriba el balance, extraccion y
   deposito") -- overrides .screen's own overflow-y:auto (which would
   otherwise scroll the whole wallet screen, balance box included, as one
   unit) specifically for #screen-wallet. */
#screen-wallet { display: flex; flex-direction: column; overflow-y: hidden; }
#screen-wallet > .wallet-balance-box,
#screen-wallet > .wallet-error,
#screen-wallet > .wallet-action { flex: 0 0 auto; }
/* Same scrollbar-gutter fix as #offersList above, same reason (2026-08-20,
   Mark: "lo mismo para las cards de activity, del mismo ancho con o sin
   barra de scroll"). */
#screen-wallet > #activityList { flex: 1 1 auto; overflow-y: auto; overflow-x: hidden; scrollbar-gutter: stable; min-height: 0; overscroll-behavior-y: contain; }

/* KNOWN GOTCHA (2026-08-20): in the ~5-6 rules right after
   `@keyframes toastEnter { from {...} to {...} }` (the ODDSFEED SECTION's
   own start), a CSS comment placed directly before a rule silently drops
   that rule's styles entirely -- confirmed repeatedly via direct
   computed-style checks (.odds-breadcrumb and .odds-search-input each
   hit this in turn as things got reshuffled), never fully root-caused.
   Comments before rules are fine everywhere else in this file (plenty
   further down in this same section). If editing near the very start of
   the ODDSFEED SECTION again: keep that short stretch comment-free, or
   verify with a real computed-style check (not just visual) before
   trusting a comment-preceded rule there. */

/* ---------- eye-candy pass (2026-08-20, "las tres mejoras") ---------- */

/* 1. Win celebration -- a distinct toast treatment plus a confetti burst
   (see celebrateWin() in app.js) for an actual bet win, not just the
   plain success/error toast every other credit event gets. Real bug
   fixed same day (Mark: "no puedo ver el confeti"): confetti pieces are
   now appended as CHILDREN of the toast element itself (position:relative
   here is what makes that anchoring work -- their own position:absolute
   is relative to this, not to the far-larger #toastStack), sized up from
   6px to 10px, and given overflow:visible so they're never clipped by
   the toast's own rounded corners as they fly outward. */
.toast.toast-win { position: relative; overflow: visible; border-left-color: var(--up); animation: toastEnter 0.25s ease, oddsWinPulse 0.6s ease 0.25s; }
@keyframes oddsWinPulse {
  0%, 100% { transform: scale(1); }
  40% { transform: scale(1.06); }
}
.odds-confetti-piece {
  position: absolute; width: 10px; height: 10px; border-radius: 2px;
  opacity: 0; animation: oddsConfettiFly 1.1s ease-out forwards;
  z-index: 5; box-shadow: 0 0 4px rgba(0,0,0,0.4);
}
@keyframes oddsConfettiFly {
  0% { transform: translate(0, 0) rotate(0deg); opacity: 1; }
  100% { transform: translate(var(--dx), var(--dy)) rotate(var(--rot)); opacity: 0; }
}

/* 2. Tap feedback -- every odds price button shrinks slightly on press,
   felt across the whole app since every tappable price uses this same
   class. */
.odd-btn { transition: transform 0.08s ease; }
.odd-btn:active { transform: scale(0.95); }

/* 3. Staggered card entrance -- match cards fade/rise in with a slight
   per-card delay (set inline per card, see matchCardHtml()'s own
   `index` param in app.js) instead of all popping in at once. `both`
   fill-mode holds the 0% state during the delay instead of flashing the
   final card at full opacity for a frame first. */
.odds-match-card { animation: oddsCardEnter 0.32s ease both; }
@keyframes oddsCardEnter {
  from { opacity: 0; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .toast.toast-win { animation: none; }
  .odds-confetti-piece { display: none; }
  .odd-btn { transition: none; }
  .odd-btn:active { transform: none; }
  .odds-match-card { animation: none; }
}
