/* ============================================================
   Matches screen + MatchCard
   ------------------------------------------------------------
   Batch prediction flow:
   editing a score stages a *draft* (no per-match save needed).
   A dirty card shows a "Sin guardar" marker; a sticky
   "Guardar todo" bar saves every staged draft in one tap.
   A small inline "Guardar" stays for one-off single saves.
   ============================================================ */

// Default is a safe no-op store (not null) so a MatchCard rendered outside a
// provider — e.g. the leagues "Partidos" tab — never crashes on destructure.
const DraftsCtx = React.createContext({
  drafts: {}, setDraft() {}, clearDraft() {}, markSaved() {}, flashIds: new Set(),
});

// a staged draft differs from what's saved → counts toward "Guardar todo"
function draftDirty(draft, myPred, isKO) {
  if (!draft) return false;
  if (!myPred) return true;
  if (myPred.homeGoals !== draft.h || myPred.awayGoals !== draft.a) return true;
  if (isKO && (myPred.advances || "") !== (draft.adv || "")) return true;
  return false;
}

// ---- engagement helpers (countdown + "fecha vigente" detection) ----
const COUNTDOWN_MS = 48 * 3600 * 1000; // show a live countdown when kickoff is this close
const URGENT_MS = 3 * 3600 * 1000;     // < 3h to kickoff → red/urgent styling

// "2d 4h" / "5h 41m" / "12m" — compact remaining-time label until kickoff
function fmtCountdown(ms) {
  if (ms <= 0) return "0m";
  const totalMin = Math.floor(ms / 60000);
  const d = Math.floor(totalMin / 1440);
  const h = Math.floor((totalMin % 1440) / 60);
  const m = totalMin % 60;
  if (d > 0) return `${d}d ${h}h`;
  if (h > 0) return `${h}h ${m}m`;
  return `${m}m`;
}
// Lima (UTC-5) calendar-day key, e.g. "2026-06-11" — for "hoy / mañana" grouping
function limaDayKey(ts) { return new Date(ts - LIMA_OFFSET_MS).toISOString().slice(0, 10); }

// Manual eased window scroll. We don't use scrollIntoView({behavior:"smooth"})
// because some webviews ignore it (it silently no-ops); a rAF tween is reliable.
function smoothScrollTo(targetY) {
  const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  if (reduce) { window.scrollTo(0, targetY); return; }
  const startY = window.scrollY;
  const dist = targetY - startY;
  if (Math.abs(dist) < 4) return;
  const dur = 380; let t0 = null;
  function step(ts) {
    if (t0 === null) t0 = ts;
    const p = Math.min(1, (ts - t0) / dur);
    const ease = p < 0.5 ? 2 * p * p : 1 - Math.pow(-2 * p + 2, 2) / 2; // easeInOutQuad
    window.scrollTo(0, Math.round(startY + dist * ease));
    if (p < 1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

// three integer percentages that sum to exactly 100 (largest-remainder rounding)
function pct3(a, b, c) {
  const tot = a + b + c;
  if (!tot) return [0, 0, 0];
  const raw = [a, b, c].map((x) => (x / tot) * 100);
  const fl = raw.map(Math.floor);
  let rem = 100 - fl.reduce((s, x) => s + x, 0);
  const order = raw.map((r, i) => [r - fl[i], i]).sort((x, y) => y[0] - x[0]);
  for (let k = 0; k < rem; k++) fl[order[k][1]]++;
  return fl;
}
// compact "hace 2 h" / "hace 12 m" relative stamp
function fmtAgo(ms) {
  const min = Math.max(0, Math.floor(ms / 60000));
  if (min < 60) return `hace ${min} m`;
  return `hace ${Math.floor(min / 60)} h`;
}

// ---- probability bars (market + crowd). Renders nothing without data. ----
// Slots between the fixture and the prediction line; read-only, never blocks
// the prediction flow. Both bars share the L/E/V color language.
const ODDS_SEG_HIDE_PCT = 8; // narrower than this → hide the % label (color carries it)

function OddsBar({ icon, name, source, meta, parts }) {
  const [pH, pD, pA] = parts;
  const seg = (w, bg, color) => (
    <div style={{ width: `${w}%`, background: bg, color, display: "flex", alignItems: "center", justifyContent: "center" }}>
      {w >= ODDS_SEG_HIDE_PCT ? `${w}%` : ""}
    </div>
  );
  return (
    <div className="qm-odds-row">
      <span className="qm-odds-label">
        <Icon name={icon} size={13} />
        <b>{name}</b>
        <span className="src">· {source}{meta ? ` · ${meta}` : ""}</span>
      </span>
      <div className="qm-odds-bar">
        {seg(pH, "var(--accent)", "#fff")}
        {seg(pD, "var(--q-grey-300, #B4B2A9)", "#2C2C2A")}
        {seg(pA, "var(--brand-2)", "#fff")}
      </div>
    </div>
  );
}

function MatchOdds({ match }) {
  const Q = window.QStore;
  const o = Q.odds(match.id);
  const market = o && o.market;
  const crowd = o && o.crowd && o.crowd.nPicks > 0 ? o.crowd : null;
  if (!market && !crowd) return null;

  // once a match is no longer open, the market snapshot is frozen (free tier =
  // pre-match only) → label it "previo al partido" instead of a live clock.
  const locked = Q.matchStatus(match) !== "open";

  return (
    <div className="qm-odds">
      <div className="qm-odds-legend">
        <span><i style={{ background: "var(--accent)" }} />Local</span>
        <span><i style={{ background: "var(--q-grey-300, #B4B2A9)" }} />Empate</span>
        <span><i style={{ background: "var(--brand-2)" }} />Visita</span>
      </div>
      {market && (
        <OddsBar
          icon="sliders" name="Mercado" source={market.source || "The Odds API"}
          meta={locked ? "previo al partido" : (market.fetchedAt ? fmtAgo(Q.NOW - market.fetchedAt) : "")}
          parts={pct3(market.pHome, market.pDraw, market.pAway)}
        />
      )}
      {crowd && (
        <OddsBar
          icon="users" name="Competidores" source={crowd.source || "Polla Quantia"}
          meta={`${crowd.nPicks} pick${crowd.nPicks === 1 ? "" : "s"}`}
          parts={pct3(crowd.cHome, crowd.cDraw, crowd.cAway)}
        />
      )}
    </div>
  );
}

function MatchCard({ match, variant }) {
  const Q = window.QStore;
  const { drafts, setDraft, clearDraft, markSaved, flashIds } = React.useContext(DraftsCtx);
  const status = Q.matchStatus(match);
  const result = Q.db.results[match.id];
  const myPred = Q.prediction(match.id);
  const home = Q.team(match.home), away = Q.team(match.away);
  const ready = Q.matchReady(match);          // knockout teams known?
  const editable = ready && status === "open";
  const isKO = Q.isKnockout(match);

  // value shown = staged draft if present, else the saved prediction, else 0
  const draft = drafts[match.id];
  const h = draft ? draft.h : (myPred ? myPred.homeGoals : 0);
  const a = draft ? draft.a : (myPred ? myPred.awayGoals : 0);
  const adv = draft ? (draft.adv || "") : (myPred ? (myPred.advances || "") : "");

  const dirty = draftDirty(draft, myPred, isKO);
  const flash = flashIds.has(match.id);

  // editing any field stages/updates the draft (always carries the full triple)
  const setH = (v) => setDraft(match.id, { h: v, a, adv });
  const setA = (v) => setDraft(match.id, { h, a: v, adv });
  const setAdv = (v) => setDraft(match.id, { h, a, adv: v });

  const saveOne = () => {
    const r = Q.actions.savePrediction(match.id, h, a, isKO && h === a ? adv : null);
    if (r.ok) {
      clearDraft(match.id);
      markSaved([match.id]);
      toast("Pronóstico guardado", "ok");
    } else {
      toast(r.error, "");
    }
  };

  const out = (status === "final" || status === "live") ? Q.outcome(Q.db.currentUserId, match) : null;
  const isExact = out && out.kind === "exact";
  const noPred = !myPred && !dirty;
  const isFeatured = !!(window.QFeatured && window.QFeatured.isFeatured(match.id));

  const TeamCell = ({ t, src }) => (
    <div className="qm-team">
      <span className="qm-flag">{t ? t.flag : "🏳️"}</span>
      <span className="qm-tname">{t ? t.name : Q.slotLabel(src)}<small>{t ? t.code : ""}</small></span>
    </div>
  );

  return (
    <div className={`qm-match ${variant === "compact" ? "compact" : ""} ${isFeatured ? "is-featured" : ""} ${status === "live" ? "is-live" : ""} ${isExact ? "is-exact" : ""} ${editable && dirty ? "is-dirty" : ""} ${editable && noPred ? "is-pending" : ""} qm-fadein`}>
      {isFeatured && (
        <div className="qm-featured-banner" title="Este partido entrega el doble de puntos">
          <span>⭐ Partido destacado</span>
          <span className="x2">Puntos ×2</span>
        </div>
      )}
      {/* top meta row */}
      <div className="top">
        <span className="grp">{roundLabel(match.group)}</span>
        <span className="when">{fmtWhen(match.kickoff)}</span>
        <span className="spacer" />
        {!ready ? <span className="qm-chip locked">Por definir</span> : <>
          {status === "open" && (() => {
            if (dirty) return <span className="qm-chip dirty"><span className="dot" />Sin guardar</span>;
            const ms = match.kickoff - Q.NOW;
            if (ms <= COUNTDOWN_MS) {
              const urgent = noPred && ms <= URGENT_MS;
              return (
                <span className={`qm-cd ${urgent ? "urgent" : "soon"}`}>
                  {urgent ? <span className="dot" /> : <Icon name="clock" size={12} stroke={2.4} />}
                  Cierra en {fmtCountdown(ms)}
                </span>
              );
            }
            return noPred
              ? <span className="qm-chip pending">Pendiente</span>
              : <span className="qm-chip open">Abierto</span>;
          })()}
          {status === "live" && <span className="qm-chip live"><span className="dot" />En juego</span>}
          {status === "locked" && <span className="qm-chip locked"><Icon name="lock" size={12} stroke={2.4} />Cerrado</span>}
          {status === "review" && <span className="qm-chip locked"><Icon name="lock" size={12} stroke={2.4} />Final por confirmar</span>}
          {status === "final" && <span className="qm-chip final">Final</span>}
        </>}
      </div>

      {/* fixture */}
      <div className="qm-fix">
        <TeamCell t={home} src={match.homeSrc} />
        <div className="qm-center">
          {editable ? (
            <div className="qm-score">
              <Stepper value={h} onChange={setH} />
              <span className="qm-vs">:</span>
              <Stepper value={a} onChange={setA} />
            </div>
          ) : !ready ? (
            <div className="qm-bigscore"><span className="g" style={{ color: "var(--q-grey-300)" }}>–</span><span className="sep">:</span><span className="g" style={{ color: "var(--q-grey-300)" }}>–</span></div>
          ) : status === "final" ? (
            <div className="qm-bigscore">
              <span className="g">{result.homeGoals}</span><span className="sep">:</span><span className="g">{result.awayGoals}</span>
            </div>
          ) : status === "live" ? (
            result && result.liveHome != null ? (
              <div className="qm-bigscore" style={{ position: "relative" }}>
                <span className="g">{result.liveHome}</span><span className="sep">:</span><span className="g">{result.liveAway}</span>
                <span style={{ position: "absolute", top: -12, left: 0, right: 0, textAlign: "center", fontSize: 9, fontWeight: 800, letterSpacing: ".08em", color: "var(--accent)" }}>🔴 EN VIVO</span>
              </div>
            ) : (
              <div className="qm-livemark"><span className="dot" />EN JUEGO</div>
            )
          ) : (
            <div className="qm-bigscore"><span className="g" style={{ color: "var(--q-grey-300)" }}>–</span><span className="sep">:</span><span className="g" style={{ color: "var(--q-grey-300)" }}>–</span></div>
          )}
        </div>
        <TeamCell t={away} src={match.awaySrc} />
      </div>

      {/* market + crowd probabilities (renders nothing without data) */}
      <MatchOdds match={match} />

      {/* knockout tie → choose who advances */}
      {editable && isKO && h === a && home && away && (
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 4, fontSize: 13 }}>
          <span style={{ color: "var(--accent)", fontWeight: 700, whiteSpace: "nowrap" }}>Empate — ¿quién avanza?</span>
          <select className="qm-sel" style={{ flex: 1, maxWidth: 240 }} value={adv} onChange={(e) => setAdv(e.target.value)}>
            <option value="">Elegir…</option>
            <option value={match.home}>{home.flag} {home.name}</option>
            <option value={match.away}>{away.flag} {away.name}</option>
          </select>
        </div>
      )}

      {/* bottom line: prediction / save / outcome */}
      {!ready ? (
        <div className="qm-predline">
          <span className="qm-mypred" style={{ display: "inline-flex", gap: 6, alignItems: "center", fontStyle: "italic" }}>
            <Icon name="lock" size={13} />Se habilita cuando se conozcan los equipos
          </span>
        </div>
      ) : editable ? (
        <div className="qm-predline">
          <span className="qm-mypred">
            {flash ? <span className="qm-saved"><Icon name="check" size={14} stroke={2.6} />Guardado</span>
              : dirty ? <span className="qm-unsaved"><span className="dot" />Sin guardar · <b>{h}–{a}</b></span>
                : myPred ? <>Tu pronóstico: <b>{myPred.homeGoals}–{myPred.awayGoals}</b></>
                  : <span style={{ color: "var(--accent)", fontWeight: 700 }}>Aún no has pronosticado</span>}
          </span>
          {dirty && (
            <button className="qm-btn qm-btn-sm qm-btn-ghost" onClick={saveOne}>
              {myPred ? "Actualizar" : "Guardar"}
            </button>
          )}
        </div>
      ) : (
        <div className="qm-predline">
          <span className="qm-mypred">
            {myPred ? <>Tu pronóstico: <b>{myPred.homeGoals}–{myPred.awayGoals}</b></>
              : status === "final" ? <>Tu pronóstico: <b>0–0</b> <span style={{ color: "var(--fg-3)", fontWeight: 400 }}>(por defecto)</span></>
                : <span style={{ fontStyle: "italic" }}>Sin pronóstico</span>}
          </span>
          {status === "final" ? <OutcomeBadge kind={out.kind} pts={out.pts} /> :
            status === "live" ? (
              out && result && result.liveHome != null
                ? <span style={{ display: "inline-flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
                    <OutcomeBadge kind={out.kind} pts={out.pts} />
                    <span style={{ color: "var(--accent)", fontWeight: 700, fontSize: 11 }}>🔴 provisional · puede cambiar al final</span>
                  </span>
                : <span className="qm-mypred" style={{ color: "var(--accent)", fontWeight: 700 }}>Resultado por confirmar</span>
            ) :
            status === "review" ? <span className="qm-mypred" style={{ color: "var(--accent)", fontWeight: 700 }}>Resultado por confirmar</span> :
              <span className="qm-mypred" style={{ display: "inline-flex", gap: 6, alignItems: "center" }}><Icon name="lock" size={13} />Esperando resultado</span>}
        </div>
      )}
    </div>
  );
}

/* sticky batch-save bar — appears whenever there are unsaved drafts */
function SaveBar({ count, onSaveAll, onDiscard, saving }) {
  if (count <= 0) return null;
  return (
    <div className="qm-savebar" role="region" aria-label="Guardar pronósticos">
      <div className="qm-savebar-inner">
        <span className="qm-savebar-count">
          <span className="n">{count}</span>
          <span className="l">sin guardar</span>
        </span>
        <button className="qm-savebar-discard" onClick={onDiscard} disabled={saving}>Descartar</button>
        <button className="qm-btn qm-btn-grad qm-savebar-go" onClick={onSaveAll} disabled={saving}>
          <Icon name="check" size={18} stroke={2.6} />
          Guardar todo
        </button>
      </div>
    </div>
  );
}

function MatchesScreen({ tweaks }) {
  const Q = useStore();
  const variant = tweaks.matchCard;
  const me = Q.current;
  const agg = Q.aggregate(Q.db.currentUserId);
  const st = Q.standings();
  const myRow = st.find((r) => r.user.userId === Q.db.currentUserId);

  const [filter, setFilter] = useState("open"); // all | pending | open | final — default to actionable

  // re-render every minute so the per-match countdowns stay live
  const [, setTick] = useState(0);
  useEffect(() => { const id = setInterval(() => setTick((t) => t + 1), 60000); return () => clearInterval(id); }, []);

  // ---- staged drafts (shared with every card via context) ----
  const [drafts, setDrafts] = useState({});
  const [flashIds, setFlashIds] = useState(() => new Set());
  const [saving, setSaving] = useState(false);
  const flashTimer = React.useRef(null);

  // reset everything when the viewed user changes (demo switch / login)
  useEffect(() => {
    setDrafts({});
    setFlashIds(new Set());
  }, [Q.db.currentUserId]);

  const setDraft = React.useCallback((id, next) => {
    setDrafts((d) => ({ ...d, [id]: { h: next.h, a: next.a, adv: next.adv || "" } }));
  }, []);
  const clearDraft = React.useCallback((id) => {
    setDrafts((d) => { const n = { ...d }; delete n[id]; return n; });
  }, []);
  const markSaved = React.useCallback((ids) => {
    setFlashIds(new Set(ids));
    clearTimeout(flashTimer.current);
    flashTimer.current = setTimeout(() => setFlashIds(new Set()), 1800);
  }, []);
  useEffect(() => () => clearTimeout(flashTimer.current), []);

  const all = Q.db.matches.slice();
  const asc = (x, y) => x.kickoff - y.kickoff;
  const desc = (x, y) => y.kickoff - x.kickoff;
  const isOpen = (m) => Q.matchStatus(m) === "open";
  const isPending = (m) => isOpen(m) && Q.matchReady(m) && !Q.prediction(m.id);
  const isEditable = (m) => Q.matchReady(m) && Q.matchStatus(m) === "open";

  // ---- group-stage progress (denominator stable through the group phase) ----
  const myUid = Q.db.currentUserId;
  const groupMatches = all.filter((m) => !Q.isKnockout(m));
  const groupTotal = groupMatches.length;
  const groupDone = groupMatches.filter((m) => Q.prediction(m.id, myUid)).length;
  const groupLeft = groupTotal - groupDone;
  const groupPct = groupTotal ? Math.round((groupDone / groupTotal) * 100) : 0;

  // ---- social proof: how I rank by completion across my personal leagues ----
  function leagueCompare() {
    const QL = window.QLeagues;
    if (!QL || !groupTotal) return null;
    const peers = new Set();
    (QL.mine || []).forEach((l) => (l.members || []).forEach((m) => { if (m.uid && m.uid !== myUid) peers.add(m.uid); }));
    if (peers.size < 2) return null; // not meaningful with fewer than 2 peers
    const done = (uid) => groupMatches.reduce((n, m) => n + (Q.prediction(m.id, uid) ? 1 : 0), 0);
    const mine = groupDone;
    const ahead = [...peers].filter((uid) => done(uid) < mine).length;
    return { pct: Math.round((ahead / peers.size) * 100), peers: peers.size };
  }
  const lc = leagueCompare();

  // ---- ranking stat: guests aren't in the company leaderboard, so show their
  // best position across their personal leagues instead of a dead "–" ----
  const isGuest = !!(me && me.guest);
  function bestLeagueRank() {
    const QL = window.QLeagues;
    if (!QL) return null;
    let best = null;
    (QL.mine || []).forEach((l) => {
      const row = (QL.ranked(l) || []).find((r) => r.you);
      if (row && (best === null || row.rank < best)) best = row.rank;
    });
    return best;
  }
  const guestRank = isGuest ? bestLeagueRank() : null;

  // ---- "fecha vigente": matches that kick off today or tomorrow (Lima) ----
  const todayKey = limaDayKey(Q.NOW);
  const tomorrowKey = limaDayKey(Q.NOW + 86400000);
  const inFocusWindow = (m) => { const k = limaDayKey(m.kickoff); return k === todayKey || k === tomorrowKey; };
  const isFocusDay = (label) => label === fmtDay(Q.NOW) || label === fmtDay(Q.NOW + 86400000);
  const focusPending = all.filter((m) => inFocusWindow(m) && isPending(m)).length;

  // every dirty draft across ALL matches (even ones hidden by the current filter)
  const dirtyMatches = all.filter((m) => isEditable(m) && draftDirty(drafts[m.id], Q.prediction(m.id), Q.isKnockout(m)));
  const dirtyCount = dirtyMatches.length;

  const saveAll = () => {
    setSaving(true);
    const list = dirtyMatches.map((m) => {
      const dr = drafts[m.id];
      const isKO = Q.isKnockout(m);
      return { matchId: m.id, homeGoals: dr.h, awayGoals: dr.a, advances: isKO && dr.h === dr.a ? (dr.adv || null) : null };
    });
    const res = Q.actions.savePredictionsBatch(list);
    // drop saved drafts; keep the ones that failed (e.g. a tie needing "advances")
    setDrafts((d) => { const n = { ...d }; res.saved.forEach((id) => delete n[id]); return n; });
    if (res.saved.length) markSaved(res.saved);
    if (!res.failed.length) {
      toast(`${res.saved.length} pronóstico${res.saved.length > 1 ? "s" : ""} guardado${res.saved.length > 1 ? "s" : ""}`, "ok");
      if (res.saved.length >= 3) fireConfetti();
    } else if (res.saved.length) {
      toast(`Guardé ${res.saved.length} · ${res.failed.length} necesita${res.failed.length > 1 ? "n" : ""} que elijas quién avanza`, "");
    } else {
      toast(res.failed[0].error || "No se pudo guardar.", "");
    }
    setSaving(false);
  };

  const discardAll = () => {
    setDrafts({});
    toast("Cambios descartados", "");
  };

  // banner / nudge → narrow to pending (closest-to-kickoff sort first) and bring
  // the first actionable card up so the user lands right on what to fill.
  const goToPending = () => {
    setFilter("pending");
    requestAnimationFrame(() => requestAnimationFrame(() => {
      const el = document.querySelector(".qm-match");
      if (!el) return;
      smoothScrollTo(Math.max(0, el.getBoundingClientRect().top + window.scrollY - 80));
    }));
  };

  let matches;
  if (filter === "pending") matches = all.filter(isPending).sort(asc);
  else if (filter === "open") matches = all.filter((m) => { const s = Q.matchStatus(m); return s === "open" || s === "live"; }).sort(asc);
  else if (filter === "final") matches = all.filter((m) => Q.matchStatus(m) === "final").sort(desc); // recent → old
  else matches = all.sort(asc);

  // group by date (insertion order follows the sort)
  const byDay = {};
  matches.forEach((m) => {
    const k = fmtDay(m.kickoff);
    (byDay[k] = byDay[k] || []).push(m);
  });

  const openCount = all.filter((m) => { const s = Q.matchStatus(m); return s === "open" || s === "live"; }).length;
  const pendingCount = all.filter(isPending).length;

  const filters = [
    ["all", "Todos"],
    ["pending", `Pendientes${pendingCount ? ` (${pendingCount})` : ""}`],
    ["open", "Por jugar"],
    ["final", "Finalizados"],
  ];

  const ctx = { drafts, setDraft, clearDraft, markSaved, flashIds };

  return (
    <DraftsCtx.Provider value={ctx}>
      <div className={dirtyCount > 0 ? "qm-has-savebar" : ""}>
        <div className="qm-eyebrow"><img src="assets/asterisk.png" alt="" />Mundial 2026 · Fase de grupos</div>
        <h1 className="qm-h">Hola, {me.name.split(" ")[0]} 👋</h1>
        <p className="qm-sub">Pronostica los marcadores antes del pitazo inicial. Ajusta los que quieras y guárdalos todos de una vez.</p>

        {/* focus banner — what's closing today / tomorrow (most actionable) */}
        {focusPending > 0 ? (
          <button className="qm-focus" onClick={goToPending}>
            <span className="blt"><Icon name="bolt" size={15} stroke={2.4} /></span>
            <span>Cierran pronto — <b>te faltan {focusPending}</b> de hoy y mañana. Complétalos antes del pitazo.</span>
            <Icon name="chevR" size={16} />
          </button>
        ) : pendingCount > 0 && filter !== "pending" ? (
          <button className="qm-pending-nudge" onClick={goToPending}>
            <span className="bolt"><Icon name="bolt" size={15} stroke={2.4} /></span>
            <span>Tienes <b>{pendingCount}</b> pronóstico{pendingCount > 1 ? "s" : ""} pendiente{pendingCount > 1 ? "s" : ""}</span>
            <Icon name="chevR" size={16} />
          </button>
        ) : null}

        {/* group-stage progress */}
        {groupTotal > 0 && (
          <div className="qm-progress">
            <div className="qm-progress-top">
              <span className="lbl">Tu fase de grupos</span>
              <span className="num"><b>{groupDone}</b> / {groupTotal}</span>
            </div>
            <div className="qm-progress-bar"><i style={{ width: `${groupPct}%` }} /></div>
            <div className="qm-progress-foot">
              <span>{groupLeft > 0 ? `Te faltan ${groupLeft} pronóstico${groupLeft > 1 ? "s" : ""}` : "¡Fase de grupos completa!"}</span>
              {lc && <span className="qm-leaguepill"><Icon name="users" size={12} stroke={2.4} />Mejor que el {lc.pct}% de tu liga</span>}
            </div>
          </div>
        )}

        {/* personal stats */}
        <div className="qm-stats">
          <div className="qm-stat accent"><div className="n">{agg.pts}</div><div className="l">Puntos</div></div>
          <div className="qm-stat ok"><div className="n">{agg.exact}</div><div className="l">Exactos</div></div>
          <div className="qm-stat gold"><div className="n">{agg.result}</div><div className="l">Resultados</div></div>
          {isGuest
            ? <div className="qm-stat"><div className="n">{guestRank ? "#" + guestRank : "–"}</div><div className="l">Tu liga</div></div>
            : <div className="qm-stat"><div className="n">{myRow ? "#" + myRow.rank : "–"}</div><div className="l">Ranking</div></div>}
        </div>

        {/* filter */}
        <div className="qm-filters">
          {filters.map(([k, l]) => (
            <button key={k} className={filter === k ? "on" : ""} onClick={() => setFilter(k)}>{l}</button>
          ))}
        </div>

        {Object.keys(byDay).length === 0 && (
          filter === "pending" ? (
            <div className="qm-empty">
              <div style={{ fontSize: 44, marginBottom: 8 }}>✅</div>
              <p style={{ fontFamily: "var(--font-head)", fontWeight: 800, color: "var(--fg-1)", fontSize: 18 }}>¡Estás al día!</p>
              <p>No tienes pronósticos pendientes por ahora.</p>
            </div>
          ) : (
            <div className="qm-empty"><img className="iso3" src="assets/iso-line-white.png" alt="" style={{ filter: "invert(.5)" }} /><p>No hay partidos en esta vista.</p></div>
          )
        )}

        {Object.entries(byDay).map(([day, ms]) => {
          const focusDay = isFocusDay(day);
          return (
            <div key={day}>
              <div className={`qm-groupbar ${focusDay ? "is-focus" : ""}`}>
                <span className="glabel">{day}</span>
                {focusDay && <span className="qm-focus-tag">Cierran pronto</span>}
                <span className="gline" />
                <span className="gcount">{ms.length} partido{ms.length > 1 ? "s" : ""}</span>
              </div>
              {ms.map((m) => <MatchCard key={m.id + Q.db.currentUserId} match={m} variant={variant} />)}
            </div>
          );
        })}
      </div>

      <SaveBar count={dirtyCount} onSaveAll={saveAll} onDiscard={discardAll} saving={saving} />
    </DraftsCtx.Provider>
  );
}

Object.assign(window, { MatchCard, MatchesScreen, DraftsCtx });
