/* AcquiCup — core screens: MatchCard, Dashboard, Calendar */
const { useState, useEffect, useRef, useMemo } = React;

/* Safety-risk tone for an outcome's points, mirroring the tiers used on the
   dashboards: ≤20 = favourite (low risk), ≤45 = mid, >45 = upset (high risk).
   `c` is a readable text/border colour, `soft` a tinted fill for the selected
   slip cell. Uses --gold (not raw --brand-yellow) for the high-risk text so it
   stays legible. */
function riskTone(pts) {
  const p = Number(pts) || 0;
  if (p <= 20) return { c: "var(--brand-navy)", soft: "color-mix(in srgb, var(--brand-navy) 12%, var(--surface-2))" };
  if (p <= 45) return { c: "var(--brand-blue)", soft: "color-mix(in srgb, var(--brand-blue) 16%, var(--surface-2))" };
  return { c: "var(--gold)", soft: "color-mix(in srgb, var(--brand-yellow) 22%, var(--surface-2))" };
}

/* ---- odds buckets (the betting-slip signature moment) ---- */
function OddsRow({ match, picked, size = "" }) {
  const cells = [[T(match.home).name + " wins", "home"], ["Draw", "draw"], [T(match.away).name + " wins", "away"]];
  return (
    <div className="row gap-6 full">
      {cells.map(([lab, key]) => {
        const on = picked === key;
        // The selected cell takes the colour of its own safety-risk tier.
        const tone = on ? riskTone(match.points[key]) : null;
        return (
          <div key={key} style={{
            flex: 1, borderRadius: 9, padding: size === "lg" ? "8px 8px" : "5px 8px",
            border: "1px solid " + (on ? tone.c : "var(--line)"),
            background: on ? tone.soft : "var(--surface-2)",
            display: "flex", flexDirection: "column", alignItems: "center", gap: 1, lineHeight: 1,
            transition: "all .12s" }}>
            <span style={{ fontFamily: "var(--fm)", fontSize: 8.5, letterSpacing: ".04em",
              color: on ? tone.c : "var(--ink-soft)", fontWeight: 500, maxWidth: "100%",
              whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{lab}</span>
            <span className="mono" style={{ fontWeight: 600, fontSize: size === "lg" ? 15 : 12,
              color: on ? tone.c : "var(--ink)" }}>{match.points[key]}</span>
          </div>
        );
      })}
    </div>
  );
}

/* Compact vertical score dial for editing a prediction in place on a match
   card: ▲ / number / ▼, sized to sit inside the narrow center column. */
function ScoreDial({ label, value, onChange }) {
  const set = (v) => onChange(Math.max(0, Math.min(15, v)));
  const empty = value === "" || value == null;
  const dialBtn = { width: 40, height: 16, padding: 0, border: "none", background: "transparent",
    cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--ink-soft)" };
  return (
    <div className="col center" style={{ gap: 2 }}>
      <button style={dialBtn} aria-label={"Increase " + label} onClick={() => set((+value || 0) + 1)}>
        <Icon name="chevronDown" size={16} stroke={2.2} style={{ transform: "rotate(180deg)" }} />
      </button>
      <span className="score-num" style={{ fontSize: 28, fontWeight: 800, width: 40, height: 42,
        lineHeight: "42px", textAlign: "center", borderRadius: 9, border: "1.5px solid var(--line)",
        background: "var(--surface-2)", color: empty ? "var(--ink-soft)" : "var(--ink)" }}>{empty ? "–" : value}</span>
      <button style={dialBtn} aria-label={"Decrease " + label} onClick={() => set((+value || 0) - 1)}>
        <Icon name="chevronDown" size={16} stroke={2.2} />
      </button>
    </div>
  );
}

function MatchCard({ match, pred, onOpen, onSave, onX2, onRemoveX2, x2On, x2Remaining }) {
  const tz = useTimezone();
  const settled = match.status === "finished";
  const live = match.status === "live";
  const locked = match.status === "locked" || live || settled;
  const ven = V(match.venue);
  // Knockout fixtures whose participants are still placeholders ("Winner Match 73",
  // "2nd Group A" — imported with an empty iso) have no real teams yet, so they
  // can't be predicted or priced. Treat them as "to be determined".
  const tbd = isTbd(match);
  const editable = !locked && !tbd && typeof onSave === "function";

  const cta = settled ? "View result" : tbd ? "Details" : locked ? "Locked" : pred ? "Edit prediction" : "Predict";
  const earned = settled && pred && pred.earned ? pred.earned.total : null;
  // Tier the earned badge by prediction quality: exact scoreline → navy, correct
  // outcome → electric blue, a miss → yellow.
  const predRight = settled && pred ? outcomeOf(pred.home, pred.away) === outcomeOf(match.homeScore, match.awayScore) : false;
  const predExact = !!(settled && pred && pred.earned && pred.earned.isExact);

  // Inline prediction edit state. Cards default to a 0–0 scoreline that is NOT
  // recorded until the player hits "Save pick" — until then the match counts as
  // un-predicted. Once saved, the card shows "Saved" and only an edited score
  // reveals an "Update pick" button. Hooks are unconditional (used if editable).
  const hasPick = pred && pred.home != null;
  const seedH = hasPick ? pred.home : 0;
  const seedA = hasPick ? pred.away : 0;
  const [eHome, setEHome] = useState(seedH);
  const [eAway, setEAway] = useState(seedA);
  const [saving, setSaving] = useState(false);
  // Re-sync from the saved prediction when it changes (detail-page edit, our own
  // optimistic save landing, or a rejected save rolling back).
  useEffect(() => { setEHome(seedH); setEAway(seedA); }, [match.id, seedH, seedA]);

  const dirty = String(eHome) !== String(seedH) || String(eAway) !== String(seedA);
  const canSave = eHome !== "" && eAway !== "";
  // A 0–0 default with no saved pick still needs saving; a saved pick only needs
  // saving once edited.
  const needsSave = !hasPick || dirty;
  // Highlight the odds cell for the current pick — the unsaved edit while editing.
  const picked = editable ? outcomeOf(eHome, eAway) : (pred ? outcomeOf(pred.home, pred.away) : null);
  const canX2 = editable && typeof onX2 === "function";
  const x2Used = !x2On && (x2Remaining ?? 0) <= 0; // both bonuses spent elsewhere
  // Max points if this exact pick lands: outcome + exact bonus, doubled when X2
  // is active on this match.
  const maxPotential = canSave ? potential(match, { home: eHome, away: eAway }) * (x2On ? 2 : 1) : 0;

  async function doSave() {
    if (!canSave || saving) return;
    setSaving(true);
    try { await onSave(match.id, { home: +eHome, away: +eAway }); }
    finally { setSaving(false); }
  }

  // Status drives a color-coded accent stripe + header tint so a card's state reads
  // at a glance: live (orange) / finished (green) / locked / needs-pick (red, an
  // editable upcoming match with no saved prediction) / upcoming (blue).
  const needsPick = editable && !hasPick;
  const statusKey = tbd ? "tbd" : live ? "live" : settled ? "finished" : locked ? "locked" : needsPick ? "needspick" : "upcoming";

  return (
    <div className={"card match-card mc-" + statusKey} style={{ overflow: "hidden" }}>
      <div className="match-card-head" style={{ display: "grid", gridTemplateColumns: "1fr auto 1fr", alignItems: "center", gap: 8, padding: "11px 16px", borderBottom: "1px solid var(--line-2)" }}>
        <span className="badge" style={{ justifySelf: "start", background: "var(--chip-bg)", border: "1px solid var(--line)" }}>{match.stage}{match.group ? " · " + match.group : ""}</span>
        <span className="muted match-card-when" style={{ justifySelf: "center", fontSize: 12 }}><span className="mc-date">{fmtMatchDate(match, tz)}</span><span className="mc-sep"> · </span><span className="mc-time">{fmtMatchTime(match, tz)}</span></span>
        <div style={{ justifySelf: "end" }}><StatusPill status={tbd ? "tbd" : match.status} minute={match.minute} /></div>
      </div>

      <div className="card-pad" style={{ padding: "16px 16px 14px" }}>
        {(() => {
          const result = settled ? outcomeOf(match.homeScore, match.awayScore) : null;
          const nameStyle = { fontWeight: 700, fontSize: 14.5, lineHeight: 1.14, minWidth: 0 };
          return (
            <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", alignItems: "center", gap: 6 }}>
              {/* home — column 1, over the "1" cell */}
              <div className="row gap-12" style={{ minWidth: 0, opacity: result === "away" ? 0.5 : 1 }}>
                <TeamBadge code={match.home} />
                <span style={nameStyle}>{T(match.home).name}</span>
              </div>
              {/* center: result / live score, or the editable "your pick" dials — over the "X" cell */}
              <div className="col center" style={{ lineHeight: 1, padding: "0 4px" }}>
                {(settled || live) ? (
                  <div className="col center" style={{ gap: 3 }}>
                    <div className="row center gap-6"><span className="score-num" style={{ fontSize: 23 }}>{match.homeScore}</span><span className="muted score-num" style={{ fontSize: 16 }}>:</span><span className="score-num" style={{ fontSize: 23 }}>{match.awayScore}</span></div>
                    {/* the player's own prediction, beneath the actual score — tinted
                        green/red once finished to show if the outcome was called right */}
                    {hasPick && (() => {
                      const right = settled && outcomeOf(pred.home, pred.away) === result;
                      const tone = !settled ? "var(--ink)" : right ? "var(--pos-tx)" : "var(--neg-tx)";
                      return (
                        <div className="col center" style={{ gap: 1 }}>
                          <span className="muted" style={{ fontSize: 8.5, letterSpacing: ".06em", textTransform: "uppercase" }}>your pick</span>
                          <span className="row center mono" style={{ gap: 3, fontWeight: 600, fontSize: 12, color: tone }}>
                            {pred.home}–{pred.away}
                            {settled && (right
                              ? <Icon name="check" size={11} stroke={2.6} style={{ color: "var(--pos-tx)" }} />
                              : <Icon name="x" size={10} stroke={2.6} style={{ color: "var(--neg-tx)" }} />)}
                          </span>
                        </div>
                      );
                    })()}
                  </div>
                ) : editable ? (
                  <div className="col center" style={{ gap: 1 }}>
                    <div className="row center" style={{ gap: 6, alignItems: "center" }}>
                      <ScoreDial label={T(match.home).code} value={eHome} onChange={setEHome} />
                      <span className="score-num muted" style={{ fontSize: 20 }}>:</span>
                      <ScoreDial label={T(match.away).code} value={eAway} onChange={setEAway} />
                    </div>
                    <span className="muted" style={{ fontSize: 8.5, letterSpacing: ".06em", textTransform: "uppercase" }}>{hasPick && !dirty ? "saved" : "your pick"}</span>
                  </div>
                ) : hasPick ? (
                  <div className="col center" style={{ gap: 2 }}><span className="mono" style={{ fontWeight: 600, fontSize: 15 }}>{pred.home}–{pred.away}</span><span className="muted" style={{ fontSize: 9, letterSpacing: ".06em", textTransform: "uppercase" }}>your pick</span></div>
                ) : (
                  <span className="muted mono" style={{ fontSize: 13 }}>vs</span>
                )}
              </div>
              {/* away — column 3, over the "2" cell */}
              <div className="row gap-12" style={{ minWidth: 0, justifyContent: "flex-end", opacity: result === "home" ? 0.5 : 1 }}>
                <span style={{ ...nameStyle, textAlign: "right" }}>{T(match.away).name}</span>
                <TeamBadge code={match.away} />
              </div>
            </div>
          );
        })()}

        <div style={{ height: 13 }}></div>
        {tbd
          ? <div className="row center" style={{ padding: "8px 0", color: "var(--ink-soft)", fontSize: 12.5, fontFamily: "var(--fm)", letterSpacing: ".03em", textAlign: "center" }}>Teams to be determined — odds open once the bracket resolves</div>
          : <OddsRow match={match} picked={picked} />}

        {editable ? (
          <>
            {/* maximum potential for the current pick (doubled when X2 is on here) */}
            <div className="row between" style={{ marginTop: 12, paddingTop: 11, borderTop: "1px solid var(--line-2)" }}>
              <span className="row gap-6" style={{ alignItems: "center" }}>
                <span className="muted" style={{ fontSize: 11.5 }}>Maximum potential</span>
                {x2On && <span className="badge gold mono" style={{ padding: "0 5px" }}><Icon name="bolt" size={9} fill />X2</span>}
              </span>
              <span className="mono" style={{ fontWeight: 700, fontSize: 15, color: canSave ? "var(--accent)" : "var(--ink-soft)" }}>
                {canSave ? maxPotential : "—"}<span className="muted" style={{ fontSize: 11, fontWeight: 500 }}> pts</span>
              </span>
            </div>
            {/* actions: Use X2 + Details on the left, Save/Saved on the right —
                kept on a single row (no wrap) so they all fit on mobile */}
            <div className="row between gap-8" style={{ marginTop: 12, minHeight: 32, flexWrap: "nowrap" }}>
              <div className="row gap-6" style={{ minWidth: 0, flexWrap: "nowrap" }}>
                {canX2 && (x2On
                  ? <button className="btn btn-sm" style={{ background: "var(--brand-yellow)", color: "#5a4400", padding: "5px 10px", whiteSpace: "nowrap" }} onClick={() => onRemoveX2 && onRemoveX2(match.id)}><Icon name="bolt" size={12} fill />Remove X2</button>
                  : x2Used
                    ? <button className="btn btn-ghost btn-sm" disabled title="You've used both X2 bonuses" style={{ padding: "5px 10px", whiteSpace: "nowrap" }}><Icon name="bolt" size={12} />X2 used</button>
                    : <button className="btn btn-ghost btn-sm" style={{ padding: "5px 10px", whiteSpace: "nowrap" }} onClick={() => onX2(match.id)}><Icon name="bolt" size={12} fill />Use X2</button>)}
                <button className="btn btn-ghost btn-sm" style={{ padding: "5px 10px", whiteSpace: "nowrap" }} onClick={() => onOpen(match)}>Details<Icon name="chevron" size={13} /></button>
              </div>
              {canSave && (needsSave
                ? <button className={"btn btn-sm " + (hasPick ? "btn-primary" : "btn-danger")} disabled={saving} onClick={doSave} style={{ whiteSpace: "nowrap" }}>
                    {saving ? "Saving…" : hasPick ? "Update pick" : "Save pick"}{!saving && <Icon name="check" size={14} stroke={2.4} />}
                  </button>
                : <span className="badge pos mono" style={{ whiteSpace: "nowrap" }}><Icon name="check" size={12} stroke={2.5} />Saved</span>)}
            </div>
          </>
        ) : (
          <div className="row between" style={{ marginTop: 13 }}>
            <div className="row gap-8" style={{ minWidth: 0 }}>
              {x2On && <span className="badge gold mono"><Icon name="bolt" size={11} fill />X2 active</span>}
              {earned != null
                ? <span className={"badge mono " + (predExact ? "navy" : predRight ? "blue" : "amber")}>
                    <Icon name={predRight ? "check" : "x"} size={11} stroke={2.5} />Earned +{earned}
                  </span>
                : ven.city
                  ? <span className="muted" style={{ fontSize: 12 }}>{ven.city}</span>
                  : null}
            </div>
            <button className={"btn btn-sm " + (locked && !settled ? "btn-ghost" : settled ? "btn-ghost" : pred ? "btn-soft" : "btn-primary")}
              disabled={locked && !settled} onClick={() => onOpen(match)}>
              {cta}{!locked && <Icon name="chevron" size={14} />}
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

/* ---- bulk prediction actions ----------------------------------------------
   Two toolbar buttons shared by the dashboard and the calendar:
     • Auto-fill — seed every still-editable, not-yet-predicted upcoming match
       from one strategy (reuses the sign-up STRATEGY_OPTIONS minus 'myself').
     • Clear all — remove every prediction on a still-editable match.
   Both delegate to the App-level store.applyStrategy / store.clearPredictions,
   which hit the batch endpoints then re-bootstrap. A local modal is used because
   app.jsx's `Modal` is private to its own (per-file) Babel script scope. */
function BulkModal({ children, onClose, width = 420 }) {
  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 200, background: "rgba(8,20,38,.5)",
      display: "flex", alignItems: "center", justifyContent: "center", padding: 20, backdropFilter: "blur(2px)" }}>
      <div className="card" onClick={(e) => e.stopPropagation()} style={{ width, maxWidth: "100%", boxShadow: "var(--shadow-pop)" }}>{children}</div>
    </div>
  );
}

// Per-strategy accent for the option's icon chip — same colored-chip language as
// the dashboard StatBox/KPI tiles: aggressive = risk red, safe = steady green,
// random = chaos gold. Falls back to the app accent for anything unmapped.
const STRATEGY_TONE = { aggressive: "var(--neg-tx)", safe: "var(--pos-tx)", random: "var(--gold)" };

// StrategyPicker — the shared body of the "Auto predict / Auto-fill" modal, used
// by BOTH the dashboard hero CTA and the calendar toolbar so the design lives in
// one place. Mirrors the app's confirm-modal layout (icon-chip header → title +
// subtitle → selectable radio cards → ghost/primary footer), matching the X2 and
// Clear-all dialogs. `verb`/`busyLabel` let each caller keep its own copy.
function StrategyPicker({ count, strategy, setStrategy, options, busy, onCancel, onApply,
  title = "Auto predict all matches", verb = "Predict", busyLabel = "Predicting…" }) {
  const matchWord = count === 1 ? "match" : "matches";
  return (
    <div className="card-pad col gap-16" style={{ padding: "22px 22px" }}>
      <div className="col gap-12">
        <div style={{ width: 48, height: 48, borderRadius: 13, flex: "none", background: "var(--accent-soft)",
          color: "var(--accent)", display: "flex", alignItems: "center", justifyContent: "center" }}>
          <Icon name="bolt" size={24} fill />
        </div>
        <div className="col gap-4">
          <h3 style={{ fontSize: 19 }}>{title}</h3>
          <span className="muted" style={{ fontSize: 13.5, lineHeight: 1.5 }}>
            Pick a strategy for the <strong>{count}</strong> upcoming {matchWord} you haven't predicted yet.
            Your saved picks stay untouched — you can edit any filled score before it locks.
          </span>
        </div>
      </div>
      <div className="col gap-8">{options.map((o) => {
        const on = strategy === o.id;
        const tone = STRATEGY_TONE[o.id] || "var(--accent)";
        return (
          <button key={o.id} onClick={() => setStrategy(o.id)} className="row gap-12"
            style={{ alignItems: "flex-start", padding: "12px 13px", borderRadius: 12, cursor: "pointer", textAlign: "left",
              background: on ? "var(--accent-soft)" : "var(--surface-2)",
              border: "1px solid " + (on ? "var(--accent)" : "var(--line)"), fontFamily: "var(--ff)" }}>
            <span style={{ width: 34, height: 34, borderRadius: 9, flex: "none", display: "flex", alignItems: "center",
              justifyContent: "center", color: tone, background: "color-mix(in srgb, " + tone + " 14%, var(--surface))" }}>
              <Icon name={o.icon} size={18} stroke={2} />
            </span>
            <div className="col grow" style={{ gap: 3, minWidth: 0 }}>
              <span style={{ fontSize: 14, fontWeight: 700, color: on ? "var(--accent)" : "var(--ink)" }}>{o.title}</span>
              <span className="muted" style={{ fontSize: 12.5, lineHeight: 1.4 }}>{o.desc}</span>
              {o.example && (
                <span className="mono" style={{ fontSize: 11, lineHeight: 1.3, color: "var(--ink-soft)",
                  background: "var(--surface-3)", padding: "3px 8px", borderRadius: 6, marginTop: 4, alignSelf: "flex-start" }}>
                  {o.example}
                </span>
              )}
            </div>
            {on && <Icon name="check" size={16} stroke={2.5} style={{ color: "var(--accent)", flex: "none", marginTop: 8 }} />}
          </button>
        );
      })}</div>
      <div className="row gap-10">
        <button className="btn btn-ghost grow" style={{ justifyContent: "center" }} disabled={busy} onClick={onCancel}>Cancel</button>
        <button className="btn btn-primary grow" style={{ justifyContent: "center" }} disabled={busy || count === 0} onClick={onApply}>
          {busy ? busyLabel : `${verb} ${count} ${matchWord}`}{!busy && <Icon name="check" size={14} stroke={2.4} />}
        </button>
      </div>
    </div>
  );
}

function PredictionBulkActions({ store }) {
  const A = window.ACQ;
  const [picking, setPicking] = useState(false);       // strategy-picker modal
  const [confirming, setConfirming] = useState(false); // clear-all confirm modal
  const [strategy, setStrategy] = useState("safe");
  const [busy, setBusy] = useState(false);

  // Strategy choices minus the blank-slate 'myself' (nothing to fill).
  const options = (window.STRATEGY_OPTIONS || []).filter((o) => o.id !== "myself");

  // Counts mirror the calendar's "missing" tab and the dashboard KPI: upcoming
  // matches with no saved pick (auto-fill also needs real teams — placeholder
  // knockouts can't be priced). These are status-based on purpose; the server's
  // isMatchEditable adds a kickoff-time check we don't replicate client-side, so
  // a just-kicked-off-but-still-"upcoming" match can be counted here yet skipped
  // by the server. That's fine — the server is authoritative and the post-action
  // toast reports the real applied/cleared count.
  const upcoming = A.MATCHES.filter((m) => m.status === "upcoming");
  const fillN = upcoming.filter((m) => !store.predictions[m.id] && !isTbd(m)).length;
  const clearN = upcoming.filter((m) => store.predictions[m.id]).length;

  // No upcoming matches at all → nothing to act on; render nothing.
  if (upcoming.length === 0) return null;

  async function doApply() {
    if (busy) return;
    setBusy(true);
    try { await store.applyStrategy(strategy); setPicking(false); }
    finally { setBusy(false); }
  }
  async function doClear() {
    if (busy) return;
    setBusy(true);
    try { await store.clearPredictions(); setConfirming(false); }
    finally { setBusy(false); }
  }

  return (
    <div className="row gap-8 wrap" style={{ alignItems: "center" }}>
      <button className="btn btn-soft btn-sm" disabled={fillN === 0}
        title={fillN === 0 ? "Every upcoming match already has a pick" : "Fill blank matches from a strategy"}
        onClick={() => setPicking(true)}>
        <Icon name="bolt" size={13} fill />Auto-fill{fillN > 0 ? ` (${fillN})` : ""}
      </button>
      <button className="btn btn-ghost btn-sm" disabled={clearN === 0}
        title={clearN === 0 ? "No editable predictions to clear" : "Remove all your upcoming-match predictions"}
        onClick={() => setConfirming(true)}>
        <Icon name="x" size={13} stroke={2.4} />Clear all{clearN > 0 ? ` (${clearN})` : ""}
      </button>

      {picking && (
        <BulkModal onClose={() => { if (!busy) setPicking(false); }} width={440}>
          <StrategyPicker count={fillN} strategy={strategy} setStrategy={setStrategy} options={options}
            busy={busy} onCancel={() => setPicking(false)} onApply={doApply}
            title="Auto-fill your predictions" verb="Fill" busyLabel="Filling…" />
        </BulkModal>
      )}

      {confirming && (
        <BulkModal onClose={() => { if (!busy) setConfirming(false); }} width={400}>
          <div className="card-pad col gap-16" style={{ padding: "22px 22px" }}>
            <div className="col gap-12">
              <div style={{ width: 48, height: 48, borderRadius: 13, flex: "none", background: "var(--neg-bg)",
                color: "var(--neg-tx)", display: "flex", alignItems: "center", justifyContent: "center" }}>
                <Icon name="x" size={24} stroke={2.4} />
              </div>
              <div className="col gap-4">
                <h3 style={{ fontSize: 19 }}>Clear all predictions?</h3>
                <span className="muted" style={{ fontSize: 13.5, lineHeight: 1.5 }}>
                  This removes your <strong>{clearN}</strong> prediction{clearN === 1 ? "" : "s"} on upcoming matches.
                  Picks on matches that have already started or finished are kept — you can re-predict any match before it locks.
                </span>
              </div>
            </div>
            <div className="row gap-10">
              <button className="btn btn-ghost grow" style={{ justifyContent: "center" }} disabled={busy} onClick={() => setConfirming(false)}>Keep them</button>
              <button className="btn btn-danger grow" style={{ justifyContent: "center" }} disabled={busy} onClick={doClear}>
                {busy ? "Clearing…" : "Clear all"}{!busy && <Icon name="x" size={14} stroke={2.4} />}
              </button>
            </div>
          </div>
        </BulkModal>
      )}
    </div>
  );
}

// Live "time to kickoff" pill for the next upcoming match. Ticks every second and
// derives the kickoff instant the same way the match cards / lock do (matchInstant in
// the SOURCE zone) so it counts down to the exact moment predictions freeze. Renders
// nothing once the match has started (or the kickoff is unparseable).
function NextMatchCountdown({ match }) {
  const [now, setNow] = useState(() => Date.now());
  useEffect(() => {
    const t = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(t);
  }, []);
  const ms = matchInstant(match && match.date, match && match.time, matchSourceTz());
  if (ms == null) return null;
  const diff = ms - now;
  if (diff <= 0) return null;
  const s = Math.floor(diff / 1000);
  const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600);
  const m = Math.floor((s % 3600) / 60), sec = s % 60;
  const text = d > 0 ? `${d}d ${h}h ${m}m` : h > 0 ? `${h}h ${m}m ${sec}s` : `${m}m ${sec}s`;
  return (
    <span className="row gap-6 nowrap" title="Time until predictions lock for the next match"
      style={{ fontSize: 13, fontWeight: 700, padding: "10px 18px", borderRadius: 999,
        justifyContent: "center", width: "100%", boxSizing: "border-box",
        background: "color-mix(in srgb, var(--hero-ink) 14%, transparent)",
        border: "1px solid color-mix(in srgb, var(--hero-ink) 22%, transparent)" }}>
      <Icon name="clock" size={13} />
      <span className="mono">{text}</span>
      <span style={{ opacity: .7, fontWeight: 600 }}>to kickoff</span>
    </span>
  );
}

/* ----------------------------- DASHBOARD ----------------------------- */
function Dashboard({ store, go, openMatch }) {
  const me = store.me;
  const A = window.ACQ;
  const tz = useTimezone();
  const [showAllPoints, setShowAllPoints] = useState(false);
  const [showAllUpcoming, setShowAllUpcoming] = useState(false);
  // "Auto predict all matches" hero action — strategy picker + busy state. Mirrors
  // PredictionBulkActions' auto-fill, but reachable straight from the dashboard.
  const [autoPicking, setAutoPicking] = useState(false);
  const [autoStrategy, setAutoStrategy] = useState("safe");
  const [autoBusy, setAutoBusy] = useState(false);
  const autoOptions = (window.STRATEGY_OPTIONS || []).filter((o) => o.id !== "myself");
  // Exclude placeholder knockout fixtures (empty-iso TBD teams) — they can't be
  // predicted or priced ("Teams to be determined"), so they don't belong in the
  // hero, the next-matches grid, or the upcoming count.
  const upcoming = A.MATCHES.filter((m) => m.status === "upcoming" && !isTbd(m));
  const myCo = A.COMPANY_RANK.find((c) => c.id === me.company);
  // Soonest upcoming match whose kickoff is still in the future — drives the hero
  // countdown. Sort by the same kickoff instant the lock uses (not payload order).
  const nowMs = Date.now();
  const nextMatch = upcoming
    .map((m) => ({ m, ms: matchInstant(m.date, m.time, matchSourceTz()) }))
    .filter((x) => x.ms != null && x.ms > nowMs)
    .sort((a, b) => a.ms - b.ms)
    .map((x) => x.m)[0];
  // Upcoming, predictable matches with no pick yet — mirrors the calendar's "Missing
  // predictions" tab; surfaces the red catch-up CTA when there's still work to do.
  const missingN = upcoming.filter((m) => !store.predictions[m.id]).length;
  // Upcoming, predictable matches that already have a pick — mirrors the calendar's
  // "Saved Predictions" tab count. Once nothing is missing, the hero swaps the
  // auto-predict CTA for a "Change my predictions" link into that tab.
  const savedN = upcoming.filter((m) => store.predictions[m.id]).length;

  async function doAutoApply() {
    if (autoBusy) return;
    setAutoBusy(true);
    try { await store.applyStrategy(autoStrategy); setAutoPicking(false); }
    finally { setAutoBusy(false); }
  }

  // Top-5 leaderboards, with the current user / their company appended (behind a
  // "⋯" gap row) when they fall outside the top 5 — so "me" is always on screen.
  const sortedUsers = [...A.USERS].sort((a, b) => a.rank - b.rank);
  const sortedCos = [...A.COMPANY_RANK].sort((a, b) => a.rank - b.rank);
  const topWithMe = (sorted, isMe, n = 5) => {
    const rows = sorted.slice(0, n).map((r) => ({ r }));
    const idx = sorted.findIndex(isMe);
    if (idx >= n) rows.push({ gap: true }, { r: sorted[idx] });
    return rows;
  };
  // Dashboard leaderboards show the top 5 (the full board lives behind the
  // "Full" button); "me" is always appended.
  const LB_COLLAPSED = 5;
  const playerRows = topWithMe(sortedUsers, (u) => u.you, LB_COLLAPSED);
  const companyRows = topWithMe(sortedCos, (c) => c.id === me.company, LB_COLLAPSED);

  // My stats: split SETTLED predictions into exact / good (right outcome, not
  // exact) / wrong (outcome missed). Percentages are of settled predictions —
  // a prediction is settled once its match has finished (`earned` is present).
  const settled = Object.values(A.MY_PREDICTIONS || {}).filter((p) => p && p.earned).length;
  // Buckets are a strict partition of `settled` (exact + good + wrong === settled). `good` is
  // a first-class server stat (right outcome that scored, NOT exact), NOT `correct - exacts`:
  // a 0-odds exact counts in `exacts` but not in `correct`, so subtracting would push a genuine
  // good prediction into "wrong". (`?? ` keeps an older payload without `good` from breaking.)
  const exactN = me.exacts;
  const goodN = me.good ?? Math.max(0, me.correct - me.exacts);
  const wrongN = Math.max(0, settled - exactN - goodN);
  // Earned points by accuracy bucket — summed from each settled pick's scored
  // `total` (includes any X2 doubling). Wrong picks score 0, so all of `me.points`
  // comes from exact + good; we surface that split beside the counts in the legend.
  let exactPts = 0, goodPts = 0;
  Object.values(A.MY_PREDICTIONS || {}).forEach((p) => {
    if (!p || !p.earned) return;
    const t = Number(p.earned.total) || 0;
    if (p.earned.isExact) exactPts += t;
    else if (p.earned.outcome > 0) goodPts += t;
  });
  const statRows = [
    { label: "Exact score", short: "Exact", value: exactN, pts: exactPts, color: "var(--brand-navy)" },
    { label: "Good prediction", short: "Good", value: goodN, pts: goodPts, color: "var(--brand-blue)" },
    { label: "Wrong prediction", short: "Wrong", value: wrongN, pts: 0, color: "var(--brand-yellow)" },
  ];

  // matchById — join each settled pick to its match so we can read the odds of the
  // outcome it backed (used by the risk profile below).
  const matchById = {};
  A.MATCHES.forEach((m) => { matchById[m.id] = m; });

  // Safety rate (computed from risk tiers, then inverted for display): the SAME settled
  // predictions split by how risky each bet was, read off the odds of the outcome backed
  // (per-match outcome points ≈ decimal-odds × 10, see src/odds.js) — the bigger the
  // points, the longer the shot you took. Buckets: low ≤20 (a favourite, decimal < ~2.0 →
  // HIGH safety), medium 21–45, high >45 (a long shot / bold draw → LOW safety). Each tier
  // tracks its pick count `n` AND the points those picks earned (`pts`, summed from the
  // scored `total`). Same `settled` denominator as the accuracy bar; a finished match is
  // always priced (backfillEstimatedOdds), so `safetyTotal` tracks `settled` and the
  // `pts===0` guard is only defensive (an unpriced match isn't "low risk").
  const risk = { low: { n: 0, pts: 0 }, med: { n: 0, pts: 0 }, high: { n: 0, pts: 0 } };
  Object.entries(A.MY_PREDICTIONS || {}).forEach(([mid, p]) => {
    if (!p || !p.earned) return;
    const m = matchById[mid];
    if (!m || !m.points) return;
    const outcome = p.home > p.away ? "home" : p.home < p.away ? "away" : "draw";
    const pts = Number(m.points[outcome]) || 0;
    if (!pts) return;
    const tier = pts <= 20 ? risk.low : pts <= 45 ? risk.med : risk.high;
    tier.n += 1;
    tier.pts += Number(p.earned.total) || 0;
  });
  const safetyTotal = risk.low.n + risk.med.n + risk.high.n;
  // Safety framing — the inverse of risk: a LOW-risk pick (favourite) is HIGH safety, a
  // long shot is LOW safety. Brand palette, high → low safety: navy → electric blue →
  // yellow (the same three anchors as the accuracy bar, read as a safe → bold ramp).
  const safetyRows = [
    { label: "High safety", short: "High", value: risk.low.n, pts: risk.low.pts, color: "var(--brand-navy)" },
    { label: "Medium safety", short: "Med", value: risk.med.n, pts: risk.med.pts, color: "var(--brand-blue)" },
    { label: "Low safety", short: "Low", value: risk.high.n, pts: risk.high.pts, color: "var(--brand-yellow)" },
  ];

  // Recent points table rows — ALL settled predictions (match finished + scored),
  // joined to the match so we can render flags, kick-off + phase, the pick, the
  // actual score, and a quality-coded points badge (exact / right outcome / miss).
  // Recent-first, mirroring the server's ACTIVITY sort (match date then time, desc).
  const recentPoints = Object.entries(A.MY_PREDICTIONS || {})
    .map(([mid, pred]) => {
      const match = matchById[mid];
      if (!match || !pred || !pred.earned) return null;
      const right = outcomeOf(pred.home, pred.away) === outcomeOf(match.homeScore, match.awayScore);
      const exact = !!pred.earned.isExact;
      return { match, pred, pts: pred.earned.total, right, exact };
    })
    .filter(Boolean)
    .sort((a, b) => {
      if (a.match.date !== b.match.date) return a.match.date < b.match.date ? 1 : -1;
      const at = a.match.time || "", bt = b.match.time || "";
      return at < bt ? 1 : at > bt ? -1 : 0;
    });
  const visiblePoints = showAllPoints ? recentPoints : recentPoints.slice(0, 5);

  const KPI = ({ label, value, sub, accent, icon }) => (
    <div className="card card-pad col gap-4" style={{ padding: "16px 18px" }}>
      <div className="row between">
        <span className="eyebrow">{label}</span>
        {icon && <Icon name={icon} size={16} style={{ color: "var(--ink-soft)" }} />}
      </div>
      <span className="mono" style={{ fontSize: 30, fontWeight: 700, color: accent || "var(--ink)", lineHeight: 1.05 }}>{value}</span>
      {sub && <span className="muted" style={{ fontSize: 12.5 }}>{sub}</span>}
    </div>
  );

  // Highlighted headline stat (total points / player rank / company rank): a
  // colored accent bar + icon chip, the big number, and the movement arrow.
  // `accuracy` (Total points only) renders a compact % bar beside the number.
  const StatBox = ({ label, value, sub, icon, color, movement, accuracy }) => (
    <div className="card col gap-8" style={{ padding: "16px 18px", boxShadow: "inset 3px 0 0 " + color + ", var(--shadow-card)" }}>
      <div className="row between">
        <div className="row gap-10">
          <span style={{ width: 32, height: 32, borderRadius: 9, flex: "none", display: "flex", alignItems: "center",
            justifyContent: "center", color: color, background: "color-mix(in srgb, " + color + " 14%, var(--surface))" }}>
            <Icon name={icon} size={17} stroke={2} />
          </span>
          <span className="eyebrow" style={{ alignSelf: "center" }}>{label}</span>
        </div>
        {movement != null && <Movement value={movement} />}
      </div>
      <div className="row gap-12" style={{ alignItems: "flex-end" }}>
        <span className="mono" style={{ fontSize: 30, fontWeight: 700, lineHeight: 1, flex: "none" }}>{value}</span>
        {accuracy && (
          <div className="col gap-4" style={{ flex: 1, minWidth: 0, paddingBottom: 3 }}>
            <div className="row between" style={{ fontSize: 11 }}>
              <span className="eyebrow">Accuracy</span>
              <span className="mono" style={{ fontWeight: 700 }}>{accuracy.pct}%</span>
            </div>
            {/* segmented track mirroring the "My stats" bar: navy=Exact, blue=Good,
                yellow=Wrong, 2px gaps between adjacent segments */}
            <div style={{ display: "flex", height: 7, borderRadius: 99, overflow: "hidden", background: "var(--surface-3)", gap: 2 }}>
              {accuracy.segments.map((s) => s.value > 0
                ? <div key={s.label} title={s.label + ": " + s.value + " of " + accuracy.total + " (" + Math.round(s.value / accuracy.total * 100) + "%)"}
                    style={{ width: "calc(" + (s.value / accuracy.total * 100) + "% - 2px)", background: s.color }}></div>
                : null)}
            </div>
          </div>
        )}
      </div>
      {sub && <span className="muted" style={{ fontSize: 12.5 }}>{sub}</span>}
    </div>
  );

  // StatBar (the shared proportional stat bar + segment-aligned legend used by both
  // My-stats bars) now lives in components.jsx so the public profile can reuse it.

  return (
    <div className="col gap-24">
      {/* hero */}
      <div className="card hero-card" style={{ background: "var(--hero-bg)", color: "var(--hero-ink)", border: "none", overflow: "hidden", position: "relative" }}>
        <div style={{ position: "absolute", inset: 0, opacity: .5, background:
          "radial-gradient(700px 240px at 88% -30%, color-mix(in srgb, var(--brand-blue) 40%, transparent), transparent)" }}></div>
        {/* pitch lines */}
        <svg style={{ position: "absolute", right: 0, top: 0, height: "100%", opacity: .12 }} width="320" height="220" viewBox="0 0 320 220" fill="none" stroke="white" strokeWidth="1.5">
          <circle cx="300" cy="110" r="60" /><line x1="300" y1="0" x2="300" y2="220" /><rect x="250" y="70" width="80" height="80" />
        </svg>
        <div className="card-pad" style={{ padding: "26px 28px", position: "relative" }}>
          <div className="row between wrap gap-16">
            <div className="col gap-2" style={{ minWidth: 0 }}>
              <span className="eyebrow" style={{ color: "color-mix(in srgb, var(--hero-ink) 70%, transparent)" }}>{upcoming.length ? upcoming[0].stage : ((A.CONFIG && A.CONFIG.tournamentName) || "World Cup 2026")}</span>
              <h1 style={{ fontSize: 27, marginTop: 6, fontWeight: 800 }}>Welcome back, {capName(me.first)}.</h1>
              <span style={{ opacity: .82, fontSize: 13.5, marginTop: 4 }}>You're #{me.rank} of {A.USERS.length} players with {me.points} points.</span>
            </div>
            {/* Countdown pill + the two action buttons share one stretch column so
                all three render at an identical width and height. */}
            <div className="col gap-8" style={{ alignItems: "stretch", width: 240, maxWidth: "100%" }}>
              {nextMatch && <NextMatchCountdown match={nextMatch} />}
              {missingN > 0 ? (
                <>
                  <button className="btn" style={{ background: "var(--neg-tx)", color: "#fff", justifyContent: "center" }}
                    onClick={() => { window.__calInitialTab = "missing"; go("matches"); }}>
                    Add missing prediction{missingN === 1 ? "" : "s"} ({missingN})<Icon name="chevron" size={15} />
                  </button>
                  <button className="btn btn-primary" style={{ justifyContent: "center" }}
                    title="Auto-fill every unpredicted upcoming match from a strategy"
                    onClick={() => setAutoPicking(true)}>
                    Auto predict all matches ({missingN})<Icon name="bolt" size={15} fill />
                  </button>
                </>
              ) : savedN > 0 ? (
                // Everything's predicted — point the user at the Saved Predictions tab
                // to review/edit their picks before kickoff locks them.
                <button className="btn btn-primary" style={{ justifyContent: "center" }}
                  title="Review and edit your saved predictions"
                  onClick={() => { window.__calInitialTab = "saved"; go("matches"); }}>
                  Change my predictions ({savedN})<Icon name="chevron" size={15} />
                </button>
              ) : null}
            </div>
          </div>
        </div>
      </div>

      {/* "Auto predict all matches" strategy picker — same flow as the calendar's
          auto-fill, but launched from the hero CTA. */}
      {autoPicking && (
        <BulkModal onClose={() => { if (!autoBusy) setAutoPicking(false); }} width={440}>
          <StrategyPicker count={missingN} strategy={autoStrategy} setStrategy={setAutoStrategy} options={autoOptions}
            busy={autoBusy} onCancel={() => setAutoPicking(false)} onApply={doAutoApply}
            title="Auto predict all matches" verb="Predict" busyLabel="Predicting…" />
        </BulkModal>
      )}

      {/* highlighted headline stats */}
      <div className="dash-stats" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(190px,1fr))", gap: 16 }}>
        <StatBox label="Total points" value={me.points} icon="trophy" color="var(--accent)" sub="Across all predictions"
          accuracy={settled ? { pct: Math.round((exactN + goodN) / settled * 100), total: settled, segments: statRows } : null} />
        <StatBox label="Player ranking" value={"#" + me.rank} icon="user" color="var(--brand-blue)" movement={me.movement} sub={"of " + A.USERS.length + " players"} />
        <StatBox label="Company ranking" value={myCo ? "#" + myCo.rank : "–"} icon="building" color="var(--gold)" movement={myCo ? myCo.movement : null} sub={myCo ? "of " + A.COMPANY_RANK.length + " companies" : "Not ranked yet"} />
      </div>

      {/* secondary KPIs — sit directly under the headline stats */}
      <div className="dash-kpis" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit,minmax(180px,1fr))", gap: 16 }}>
        {(() => { const avg = settled ? me.points / settled : 0; return (
          <KPI label="Points / Match" value={settled ? avg.toFixed(1) : "–"} accent="var(--accent)" sub={settled ? `${me.points} pts · ${settled} settled` : "No settled predictions yet"} icon="target" />
        ); })()}
        <KPI label="Best match" value={`+${me.best}`} accent="var(--accent)" sub={me.best > 0 ? "Top scoring prediction" : "No finished matches yet"} icon="star" />
        {(() => { const used = me.x2Count || 0; const left = Math.max(0, 2 - used); return (
          <KPI label="X2 bonus" value={`${used}/2 used`} accent={left > 0 ? "var(--gold)" : "var(--ink-soft)"} sub={left > 0 ? `${left} left · doubles a match` : "Both spent this tournament"} icon="bolt" />
        ); })()}
        {(() => { const total = (A.MATCHES || []).length; const played = (A.MATCHES || []).filter((m) => m.status === "finished").length; const pct = total ? Math.round((played / total) * 100) : 0; return (
          <KPI label="Completion Rate" value={total ? `${pct}%` : "–"} accent="var(--accent)" sub={total ? `${played} of ${total} matches played` : "No matches yet"} icon="check" />
        ); })()}
      </div>

      {/* top rankings — just below the welcome banner; "me" always shown */}
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 20, alignItems: "start" }} className="dash-grid">
        {/* top players */}
        <div className="card" style={{ overflow: "hidden" }}>
          <div className="row between" style={{ padding: "13px 16px 11px" }}>
            <span className="eyebrow">Top players</span>
            <button className="btn btn-ghost btn-sm" onClick={() => go("leaderboard")}>Full<Icon name="chevron" size={13} /></button>
          </div>
          <div style={{ overflowX: "auto" }}>
            <table className="lb">
              <thead><tr><th style={{ width: 62 }}>Rank</th><th>Player</th><th style={{ textAlign: "right" }}>Pts</th></tr></thead>
              <tbody>
                {playerRows.map((item, i) => item.gap
                  ? <tr key={"pg" + i}><td colSpan={3} className="muted" style={{ textAlign: "center", padding: "2px 0", fontSize: 15 }}>⋯</td></tr>
                  : <tr key={item.r.id} className={"hov " + (item.r.you ? "me-row" : "")}
                      style={{ cursor: store.openUser ? "pointer" : undefined }}
                      onClick={() => store.openUser && store.openUser(item.r)}>
                      <td><span className="rank-num">{item.r.rank}</span></td>
                      <td><div className="row gap-10" style={{ minWidth: 0 }}><CompanyLogo id={item.r.company} size={24} /><span className="nowrap" style={{ fontWeight: item.r.you ? 800 : 600, fontSize: 13.5, overflow: "hidden", textOverflow: "ellipsis" }}>{userName(item.r)}{item.r.you ? " · you" : ""}</span>{item.r.isAi && <AiBadge style={{ marginLeft: 6 }} />}</div></td>
                      <td style={{ textAlign: "right" }}><span className="mono" style={{ fontWeight: 700, fontSize: 14 }}>{item.r.points}</span></td>
                    </tr>
                )}
              </tbody>
            </table>
          </div>
        </div>

        {/* top companies */}
        <div className="card" style={{ overflow: "hidden" }}>
          <div className="row between" style={{ padding: "13px 16px 11px" }}>
            <span className="eyebrow">Top companies</span>
            <button className="btn btn-ghost btn-sm" onClick={() => go("leaderboard")}>Full<Icon name="chevron" size={13} /></button>
          </div>
          <div style={{ overflowX: "auto" }}>
            <table className="lb">
              <thead><tr><th style={{ width: 62 }}>Rank</th><th>Company</th><th style={{ textAlign: "right" }}>Avg</th></tr></thead>
              <tbody>
                {companyRows.map((item, i) => item.gap
                  ? <tr key={"cg" + i}><td colSpan={3} className="muted" style={{ textAlign: "center", padding: "2px 0", fontSize: 15 }}>⋯</td></tr>
                  : <tr key={item.r.id} className={"hov " + (item.r.id === me.company ? "me-row" : "")}
                      style={{ cursor: store.openCompany ? "pointer" : undefined }}
                      onClick={() => store.openCompany && store.openCompany(item.r)}>
                      <td><span className="rank-num">{item.r.rank}</span></td>
                      <td><div className="row gap-10" style={{ minWidth: 0 }}><CompanyLogo id={item.r.id} size={24} /><span className="nowrap" style={{ fontWeight: item.r.id === me.company || item.r.isHost ? 800 : 600, fontSize: 13.5, overflow: "hidden", textOverflow: "ellipsis" }}>{item.r.name}{item.r.id === me.company ? " · you" : ""}</span>{item.r.isHost && <span className="badge blue" style={{ marginLeft: 8, flex: "none", border: "1px solid color-mix(in srgb, var(--accent) 30%, transparent)" }}>Host</span>}{item.r.isAi && <AiBadge style={{ marginLeft: 8 }} />}</div></td>
                      <td style={{ textAlign: "right" }}><span className="mono" style={{ fontWeight: 700, fontSize: 14 }}>{item.r.avgTop3}</span></td>
                    </tr>
                )}
              </tbody>
            </table>
          </div>
        </div>
      </div>

      {/* my stats — one card, two proportional bars: Accuracy rate (exact / good / wrong)
          and Safety rate (high / medium / low by the odds backed). Each segment shows its
          share, the number of predictions, and the points those picks earned. */}
      <div className="card card-pad col gap-16" style={{ padding: "20px 22px" }}>
        <div className="row between wrap gap-8">
          <span className="eyebrow">My stats</span>
          <span className="muted" style={{ fontSize: 12.5 }}>{settled ? settled + " settled prediction" + (settled === 1 ? "" : "s") : "No settled predictions yet"}</span>
        </div>
        {settled === 0
          ? <span className="muted" style={{ fontSize: 13.5 }}>Once your matches finish, you'll see how your predictions break down here.</span>
          : <div className="col gap-24">
              {/* 1 — accuracy rate: settled picks split exact / good (right outcome) / wrong */}
              <div className="col gap-12">
                <div className="col gap-2" style={{ minWidth: 0 }}>
                  <span style={{ fontWeight: 700, fontSize: 14 }}>Accuracy rate</span>
                  <span className="muted" style={{ fontSize: 11.5 }}>How your settled picks scored — exact score, right outcome, or missed</span>
                </div>
                <StatBar rows={statRows} total={settled} />
              </div>
              {/* 2 — safety rate: settled picks split by the odds of the outcome backed
                  (high safety = favourite ≤20 pts, medium 21–45, low = long shot >45);
                  brand ramp navy → blue → yellow */}
              <div className="col gap-12">
                <div className="col gap-2" style={{ minWidth: 0 }}>
                  <span style={{ fontWeight: 700, fontSize: 14 }}>Safety rate</span>
                  <span className="muted" style={{ fontSize: 11.5 }}>How safe your bets were — by the odds of the outcome you backed</span>
                </div>
                {safetyTotal === 0
                  ? <span className="muted" style={{ fontSize: 12.5 }}>No priced bets settled yet.</span>
                  : <StatBar rows={safetyRows} total={safetyTotal} />}
              </div>
              {/* The points-by-type / points-by-safety donuts now live on the player's
                  own profile page (alongside the rank charts), reached via the "View
                  more stats" button below — keeping the dashboard's stats card light. */}
              <div className="row center" style={{ paddingTop: 4 }}>
                <button className="btn btn-ghost btn-sm" onClick={() => store.openUser && store.openUser(me)}
                  title="See your full points breakdown and rank history on your profile">
                  View more stats<Icon name="chevron" size={13} />
                </button>
              </div>
            </div>}
      </div>

      {/* recent points — single-column table, centred: match (with flags) ·
          prediction · score · points · kick-off · phase. The betting-relevant
          columns lead so on a narrow phone they read without horizontal scroll,
          and the lower-priority kick-off/phase metadata trails to the right (the
          kick-off stays on a single row). Points badge colour-coded like the
          match card (navy = exact, blue = right outcome, amber = miss). Shows the 5
          most recent by default with a "Show more" toggle for the rest. */}
      <div className="card" style={{ overflow: "hidden" }}>
        <div className="row between" style={{ padding: "13px 16px 11px" }}>
          <span className="eyebrow">Recent points</span>
          {recentPoints.length > 0 && <span className="muted" style={{ fontSize: 12.5 }}>{recentPoints.length} settled</span>}
        </div>
        {recentPoints.length === 0
          ? <div className="card-pad" style={{ padding: "0 16px 16px" }}><span className="muted" style={{ fontSize: 13.5 }}>No points yet — they'll appear here once matches finish.</span></div>
          : <>
              <div style={{ overflowX: "auto" }}>
                <table className="lb dash-recent-table" style={{ textAlign: "center" }}>
                  <thead><tr>
                    <th style={{ textAlign: "center" }}>Match</th>
                    <th style={{ textAlign: "center" }}>Prediction</th>
                    <th style={{ textAlign: "center" }}>Score</th>
                    <th style={{ textAlign: "center" }}>Points</th>
                    <th style={{ textAlign: "center" }}>Kick-off</th>
                    <th style={{ textAlign: "center" }}>Phase</th>
                  </tr></thead>
                  <tbody>
                    {visiblePoints.map((r) => (
                      <tr key={r.match.id}>
                        <td>
                          <div className="row gap-8" style={{ alignItems: "center", justifyContent: "center", minWidth: 0 }}>
                            <TeamBadge code={r.match.home} size="sm" />
                            <span className="mono" style={{ fontWeight: 700, fontSize: 12.5 }}>{T(r.match.home).code}</span>
                            <span className="muted" style={{ fontSize: 11 }}>v</span>
                            <span className="mono" style={{ fontWeight: 700, fontSize: 12.5 }}>{T(r.match.away).code}</span>
                            <TeamBadge code={r.match.away} size="sm" />
                          </div>
                        </td>
                        <td><span className="mono" style={{ fontWeight: 600, fontSize: 13 }}>{r.pred.home}–{r.pred.away}</span></td>
                        <td><span className="mono" style={{ fontWeight: 700, fontSize: 13 }}>{r.match.homeScore}–{r.match.awayScore}</span></td>
                        <td>
                          <span className={"badge mono " + (r.exact ? "navy" : r.right ? "blue" : "amber")}>
                            <Icon name={r.right ? "check" : "x"} size={11} stroke={2.5} />+{r.pts}
                          </span>
                        </td>
                        <td><span className="muted nowrap" style={{ fontSize: 12.5 }}>{fmtMatchDate(r.match, tz)}</span></td>
                        <td><span className="badge" style={{ background: "var(--chip-bg)" }}>{r.match.stage}{r.match.group ? " · " + r.match.group : ""}</span></td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
              {recentPoints.length > 5 && (
                <div className="row center" style={{ padding: "12px 16px", borderTop: "1px solid var(--line-2)" }}>
                  <button className="btn btn-ghost btn-sm" onClick={() => setShowAllPoints((v) => !v)}>
                    {showAllPoints ? "Show less" : "Show " + (recentPoints.length - 5) + " more"}
                    <Icon name="chevronDown" size={14} style={{ transform: showAllPoints ? "rotate(180deg)" : "none" }} />
                  </button>
                </div>
              )}
            </>}
      </div>

      {/* next matches — two cards per row on desktop */}
      <div className="col gap-12">
        <div className="row between wrap gap-10"><h3 style={{ fontSize: 16 }}>Next matches to predict</h3><div className="row gap-8 wrap" style={{ alignItems: "center" }}><PredictionBulkActions store={store} /><button className="btn btn-ghost btn-sm" onClick={() => go("matches")}>All matches</button></div></div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, alignItems: "start" }} className="dash-grid">
          {(showAllUpcoming ? upcoming : upcoming.slice(0, 4)).map((m) => (
            <MatchCard key={m.id} match={m} pred={store.predictions[m.id]} x2On={store.x2MatchIds.includes(m.id)} x2Remaining={2 - store.x2MatchIds.length} onOpen={openMatch} onSave={store.savePred} onX2={store.setX2} onRemoveX2={store.removeX2} />
          ))}
        </div>
        {upcoming.length > 4 && (
          <div className="row center">
            <button className="btn btn-ghost btn-sm" onClick={() => setShowAllUpcoming((v) => !v)}>
              {showAllUpcoming ? "Show less" : "Show " + (upcoming.length - 4) + " more"}
              <Icon name="chevronDown" size={14} style={{ transform: showAllUpcoming ? "rotate(180deg)" : "none" }} />
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

/* ----------------------------- CALENDAR ----------------------------- */
function Calendar({ store, openMatch }) {
  const A = window.ACQ;
  // A dashboard CTA can ask the calendar to open on a specific tab (e.g. "Predict next
  // matches" → Upcoming). Consume the one-shot hint so a later plain visit defaults to All.
  const [tab, setTab] = useState(() => { const t = window.__calInitialTab; window.__calInitialTab = null; return t || "all"; });
  // Stored match dates are UTC ("YYYY-MM-DD"), so anchor "today" in UTC too.
  const todayStr = new Date().toISOString().slice(0, 10);
  const tabs = [
    ["all", "All Matches"], ["missing", "Missing predictions"], ["saved", "Saved Predictions"],
    ["upcoming", "Upcoming"], ["finished", "Finished"], ["today", "Today"],
  ];
  const list = A.MATCHES.filter((m) => {
    // Hide placeholder knockout fixtures (no real teams yet) — "Teams to be determined".
    if (isTbd(m)) return false;
    const p = store.predictions[m.id];
    switch (tab) {
      case "all": return true;
      case "missing": return m.status === "upcoming" && !p && !isTbd(m);
      case "saved": return m.status === "upcoming" && !!p;
      case "upcoming": return m.status === "upcoming" || m.status === "locked";
      case "finished": return m.status === "finished";
      case "today": return m.date === todayStr;
      default: return true;
    }
  });
  // group by date
  const byDate = {};
  list.forEach((m) => { (byDate[m.date] = byDate[m.date] || []).push(m); });
  const dates = Object.keys(byDate).sort();

  return (
    <div className="col gap-20">
      <PageHead eyebrow="Fixtures" title="Match calendar"
        sub="Predict every upcoming match before kickoff. Predictions lock automatically the moment a match starts."
        right={<PredictionBulkActions store={store} />} />
      <div className="row" style={{ overflowX: "auto", paddingBottom: 4 }}>
        <div className="tabs">{tabs.map(([id, lab]) => <button key={id} className={tab === id ? "on" : ""} onClick={() => setTab(id)}>{lab}{id === "missing" ? ` (${A.MATCHES.filter((m) => m.status === "upcoming" && !store.predictions[m.id] && !isTbd(m)).length})` : ""}{id === "saved" ? ` (${A.MATCHES.filter((m) => m.status === "upcoming" && store.predictions[m.id] && !isTbd(m)).length})` : ""}</button>)}</div>
      </div>
      {dates.length === 0 && <div className="card card-pad muted" style={{ textAlign: "center", padding: 40 }}>No matches in this view.</div>}
      {dates.map((d) => (
        <div key={d} className="col gap-12">
          <div className="row gap-10 cal-date" style={{ position: "sticky", top: 0 }}>
            <span style={{ fontWeight: 700, fontSize: 14 }}>{fmtDay(d)}</span>
            <span className="muted" style={{ fontSize: 12.5 }}>{byDate[d].length} matches</span>
          </div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(min(100%,330px),1fr))", gap: 16 }}>
            {byDate[d].map((m) => <MatchCard key={m.id} match={m} pred={store.predictions[m.id]} x2On={store.x2MatchIds.includes(m.id)} x2Remaining={2 - store.x2MatchIds.length} onOpen={openMatch} onSave={store.savePred} onX2={store.setX2} onRemoveX2={store.removeX2} />)}
          </div>
        </div>
      ))}
    </div>
  );
}

Object.assign(window, { MatchCard, OddsRow, Dashboard, Calendar, PredictionBulkActions });
