/* ============================================================
   Modals — Trade Machine (full), Sign FA (compact), Add Player.
   All use the same `.scrim` shell.
   ============================================================ */

function Modal({ onClose, children, wide }) {
  useEffect(() => {
    function onKey(e) { if (e.key === "Escape") onClose(); }
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);
  return (
    <div className="scrim" onClick={onClose}>
      <div className={`modal ${wide ? "wide" : ""}`} onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>
  );
}

/* League-wide signable free agents (UFA/RFA) for the Sign-FA search — i (backlog).
   Excludes the managed team's OWN FAs (those are re-signed below its roster, WITH
   bird rights). Signing another team's FA grants no Bird rights → always non-Bird.
   Each: { name, team, status, position, yos, prior }. */
function signableFreeAgents(teams, exceptCode) {
  if (!teams) return [];
  const out = [];
  for (const code of Object.keys(teams)) {
    if (code === exceptCode) continue;
    for (const p of (teams[code].players || [])) {
      if (p.offseasonStatus === "UFA" || p.offseasonStatus === "RFA") {
        out.push({ name: p.name, team: code, status: p.offseasonStatus,
                   position: p.position || "SF", yos: p.yearsOfExperience, prior: p.priorSeasonSalary || 0,
                   qualifyingOffer: p.qualifyingOffer, capHold: p.capHold });   // RFA: surface the QO + hold (display)
      }
    }
  }
  out.sort((a, b) => a.name.localeCompare(b.name));
  return out;
}

/* ============================================================
   Sign FA — quick external signing flow
   ============================================================ */
function SignFAModal({ onClose, state, dispatch, allTeams, capBase, apronTotal, unlikely, additions }) {
  const [name, setName] = useState("");
  const [salary, setSalary] = useState("8.0");
  const [years, setYears] = useState(2);
  const [position, setPosition] = useState("SG");
  const [tool, setTool] = useState("MLE");
  const [option, setOption] = useState(null);   // final-year P/T option
  const [raisePct, setRaisePct] = useState(0.05);
  const [raises, setRaises] = useState([]);
  const [pyExpanded, setPyExpanded] = useState(false);
  const [picked, setPicked] = useState(null);    // selected real FA (null = manual entry)
  const [showList, setShowList] = useState(false);
  const raiseRate = 0.05;                        // outside FA = non-Bird (5%)

  // i (backlog): search the league's signable free agents (other teams' UFA/RFA).
  const faPool = signableFreeAgents(allTeams || (typeof window !== "undefined" && window.__tmData && window.__tmData.teams), state.team);
  const query = name.trim().toLowerCase();
  const matches = query.length >= 1 ? faPool.filter(f => f.name.toLowerCase().includes(query)).slice(0, 8) : [];
  function pickFa(f) { setName(f.name); if (f.position) setPosition(f.position); setPicked(f); setShowList(false); }
  function onNameChange(v) { setName(v); setPicked(null); setShowList(true); }

  // b (backlog): gate tools by the managed team's cap STATUS + track exception pools.
  // capBase = holds-INCLUSIVE Team Salary (§6(n) standing-room gate) — NOT the holds-free
  // apron total. An over-cap team (holds kept) must NOT be offered Cap space / Room MLE;
  // it earns room only by renouncing holds (which drops capBase). Mirrors the Trade
  // Machine fix (7571b2a / deriveExceptions 09f4d0f).
  const eligibleTools = (typeof eligibleSigningTools === "function") ? eligibleSigningTools(capBase, apronTotal) : ["Cap space","MLE","TPMLE","BAE","Room MLE","Vet min"];
  const effTool = eligibleTools.includes(tool) ? tool : (eligibleTools[0] || "Vet min");
  const excRem = (typeof exceptionRemainingFor === "function") ? exceptionRemainingFor(additions, effTool) : null;
  // Phase 2 — available cap room (holds-inclusive). For a Cap-space signing the binding
  // ceiling is min(player max, room); capBase already counts committed signings, so room
  // auto-decrements as signings stack (no separate ledger needed).
  const capRoom = Number.isFinite(capBase) ? Math.max(0, CAP_2026.cap - capBase) : null;
  // Phase 3b — the team's prior exception signings this cap year (by .source) + the pending tool,
  // mapped to engine keys, for signingSetViolations (Room MLE vs NT/TP-MLE+BAE camp-lock; one-MLE).
  const TOOL_KEY = { "MLE": "nt_mle", "TPMLE": "tp_mle", "BAE": "bae", "Room MLE": "room_mle" };
  const usedExceptions = [...(additions || []).map(a => TOOL_KEY[a.source]).filter(Boolean), TOOL_KEY[effTool]].filter(Boolean);
  // Phase 4d: the signing is bound by the STRICTER of (a) any PRIOR hard cap on this team
  // (earlier NT-MLE/BAE/TP-MLE signing or applied trade) and (b) the cap the chosen tool
  // SELF-imposes on the very signing that triggers it — NT-MLE/BAE can't cross the 1st apron,
  // TP-MLE can't cross the 2nd (§2(e)/§6(n)). Room MLE / Cap space / Vet min: none.
  const _scHC = (typeof window !== "undefined" && typeof window.curSc === "function") ? window.curSc(state) : null;
  const _storedAt = (_scHC && typeof window.strictestHardCap === "function") ? window.strictestHardCap((_scHC.hardCaps || {})[state.team]) : null;
  const _priorCeil = _storedAt === "firstApron" ? CAP_2026.apron1 : _storedAt === "secondApron" ? CAP_2026.apron2 : Infinity;
  const _toolCeil = (effTool === "MLE" || effTool === "BAE") ? CAP_2026.apron1 : effTool === "TPMLE" ? CAP_2026.apron2 : Infinity;
  const _hcCeil = Math.min(_priorCeil, _toolCeil);
  const hardCapCeiling = Number.isFinite(_hcCeil) ? _hcCeil : null;

  const base = parseSalaryFromMField(salary) || 0;
  const multiYear = state.multiYear;
  const adjustRaises = state.adjustRaises;
  const yrs = multiYear ? years : 1;
  const effRate = raisePct ?? raiseRate;
  const usePerYear = adjustRaises && pyExpanded && yrs > 2;
  const showSingle = adjustRaises && !usePerYear;
  const raisesArr = Array.from({ length: Math.max(0, yrs - 1) }, (_, i) => raises[i] ?? raiseRate);
  const setRaiseAt = (i, v) => { const a = raisesArr.slice(); a[i] = v; setRaises(a); };
  const expandPY = () => { setRaises(Array.from({ length: Math.max(0, yrs - 1) }, () => effRate)); setPyExpanded(true); };
  const totalOpts = usePerYear ? { raises: raisesArr } : showSingle ? { rate: effRate } : { rate: raiseRate };
  const yearSal = contractYearSalaries(base, yrs, totalOpts);
  const total = contractTotalFrom(base, yrs, totalOpts);

  // G1 (a): engine-backed legality. Outside FA = non-Bird "new" signing;
  // phase i refines birdStatus/yos/prior from the picked free agent + team.
  const legal = (typeof signingLegality === "function") ? signingLegality({
    birdStatus: "other", instrument: "new",
    yos: picked ? picked.yos : null, priorSalary: picked ? picked.prior : 0,
    years: yrs, startingSalary: base,
    raisePct: usePerYear ? null : effRate,
    raises: usePerYear ? raisesArr : null,
    tool: effTool,
    exceptionRemaining: excRem ? excRem.left : null,
    availableRoom: capRoom,
    usedExceptions,
    hardCapCeiling,
    apronAfter: Number.isFinite(apronTotal) ? apronTotal + (Number(unlikely) || 0) + base : null,
  }) : { violations: [], blocked: false, maxYears: 4 };

  function submit() {
    const n = parseSalaryFromMField(salary);
    if (!name.trim() || !n || legal.blocked) return;
    const player = { id: "fa-" + Date.now(), name: name.trim(), position, salary: n, years: yrs, option: yrs > 1 ? option : null, source: effTool };
    if (adjustRaises && yrs > 1) {
      if (pyExpanded) player.raises = raisesArr.slice(0, yrs - 1);
      else player.raisePct = raisePct;
    }
    dispatch({ type: "ADD_PLAYER", player });
    // Phase 4d: persist the signing's hard cap. NT-MLE ("MLE") + BAE → 1st apron;
    // Taxpayer MLE ("TPMLE") → 2nd apron. Room MLE / Cap space / Vet min → none.
    // Tag with the addition id (signId) so REMOVE_ADDITION clears it.
    const HC_AT = { "MLE": "firstApron", "BAE": "firstApron", "TPMLE": "secondApron" };
    if (HC_AT[effTool]) dispatch({ type: "SET_HARD_CAP", team: state.team, at: HC_AT[effTool], source: effTool, signId: player.id });
    dispatch({ type: "SET_TOAST", toast: `Signed ${name.trim()} · ${years}y · ${fmt$(n)}` });
    onClose();
  }

  return (
    <Modal onClose={onClose}>
      <div className="modal-head">
        <div>
          <h3>Sign a free agent</h3>
          <div className="desc">External FA — counts against your tools.</div>
        </div>
      </div>
      <div className="modal-body">
        <div className="form-row fa-search">
          <label>Free agent
            {picked && <span className={`fa-badge ${picked.status === "RFA" ? "rfa" : "ufa"}`}>{picked.status}</span>}
          </label>
          <input type="text" placeholder="Search free agents…" value={name}
                 onChange={(e) => onNameChange(e.target.value)}
                 onFocus={() => setShowList(true)}
                 onBlur={() => setTimeout(() => setShowList(false), 150)}
                 autoFocus />
          {showList && matches.length > 0 && (
            <div className="fa-list">
              {matches.map(f => (
                <button type="button" key={f.name + f.team} className="fa-opt" onClick={() => pickFa(f)}>
                  <span className="fa-opt-name">{f.name}</span>
                  <span className="fa-opt-meta">{f.team} · <span className={f.status === "RFA" ? "rfa" : "ufa"}>{f.status}</span></span>
                </button>
              ))}
            </div>
          )}
          {picked && picked.status === "RFA" && (
            <div className="fa-match-note">Restricted FA — <b>{picked.team}</b> can match any offer.
              {(picked.qualifyingOffer != null || picked.capHold != null) && (
                <> · QO <b>{fmt$(picked.qualifyingOffer != null ? picked.qualifyingOffer : picked.capHold)}</b> · hold {fmt$(picked.capHold)}</>
              )}
            </div>
          )}
        </div>
        <div className="form-grid">
          <div className="form-row">
            <label>Annual salary</label>
            <input type="text" value={salary} onChange={(e) => setSalary(e.target.value)} placeholder="8.0M, 9500000…" />
          </div>
          {multiYear && (
            <div className="form-row">
              <label>Years</label>
              <select value={years} onChange={(e) => setYears(+e.target.value)}>
                {Array.from({ length: legal.maxYears }, (_, i) => i + 1).map(y => <option key={y} value={y}>{y} year{y > 1 ? "s" : ""}</option>)}
              </select>
            </div>
          )}
        </div>
        <div className="form-grid">
          <div className="form-row">
            <label>Position</label>
            <select value={position} onChange={(e) => setPosition(e.target.value)}>
              {["PG","SG","SF","PF","C"].map(p => <option key={p} value={p}>{p}</option>)}
            </select>
          </div>
          <div className="form-row">
            <label>Signing tool</label>
            <select value={effTool} onChange={(e) => setTool(e.target.value)}>
              {eligibleTools.map(t => <option key={t} value={t}>{t}</option>)}
            </select>
          </div>
        </div>
        {multiYear && (
          <div className="form-row">
            <label>Final-year option</label>
            <div className="opt-seg">
              {[["none","None"],["player","Player"],["team","Team"]].map(([v, lbl]) => (
                <button key={v} type="button" disabled={yrs <= 1}
                        className={(yrs <= 1 ? "none" : (option || "none")) === v ? "on" : ""}
                        onClick={() => { if (yrs > 1) setOption(v === "none" ? null : v); }}>{lbl}</button>
              ))}
            </div>
          </div>
        )}
        {multiYear && yrs > 1 && base > 0 && (
          <div className="contract-total">
            {showSingle ? (
              <>Total ≈ {fmt$(total)} over {yrs} yrs · raises <RaiseStepper value={effRate} max={raiseRate} onChange={setRaisePct} />
                {yrs > 2 && <button type="button" className="py-toggle" onClick={expandPY}>Per year ▾</button>}
              </>
            ) : usePerYear ? (
              <>Total ≈ {fmt$(total)} over {yrs} yrs · per-year <button type="button" className="py-toggle" onClick={() => setPyExpanded(false)}>▴</button></>
            ) : (
              <>Total ≈ up to {fmt$(total)} over {yrs} yrs · max {Math.round(raiseRate * 100)}% raises</>
            )}
          </div>
        )}
        {usePerYear && multiYear && yrs > 1 && base > 0 && (
          <div className="raise-peryear">
            {raisesArr.map((r, i) => (
              <div className="raise-py-row" key={i}>
                <span className="resign-hdr">Yr {i + 2}</span>
                <RaiseStepper value={r} max={raiseRate} onChange={(v) => setRaiseAt(i, v)} />
                <span className="raise-py-amt">{fmt$(yearSal[i + 1])}</span>
              </div>
            ))}
          </div>
        )}
        {base > 0 && legal.violations.length > 0 && (
          <div className="sign-legality">
            {legal.violations.map((v, i) => (
              <div key={i} className={`sl-row ${v.severity}`}>
                <span className="sl-ic">{v.severity === "block" ? "⛔" : "⚠"}</span>
                <span>{v.msg}</span>
              </div>
            ))}
          </div>
        )}
        <div style={{ padding: 11, borderRadius: 8, background: "var(--bg-row)", border: "1px solid var(--line)", fontSize: 12, color: "var(--text-dim)", display: "flex", gap: 10, alignItems: "flex-start" }}>
          <span style={{ width: 16, height: 16, marginTop: 1, borderRadius: 50, background: "var(--info-bg)", color: "var(--info)", display: "grid", placeItems: "center", fontSize: 10, fontWeight: 800, flexShrink: 0 }}>i</span>
          <div>
            Using <b style={{ color: "var(--text)" }}>{effTool}</b> {(effTool === "MLE" || effTool === "BAE") ? "triggers a hard cap at the 1st apron." : effTool === "TPMLE" ? "triggers a hard cap at the 2nd apron." : "is unrestricted."}
            {excRem && <><br /><b style={{ color: excRem.left <= 0 ? "var(--danger)" : "var(--text)" }}>{fmt$(excRem.left)}</b> of {fmt$(excRem.amount)} {effTool} left{excRem.used > 0 ? ` (${fmt$(excRem.used)} used)` : ""}.</>}
          </div>
        </div>
      </div>
      <div className="modal-foot">
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" disabled={!name.trim() || legal.blocked} onClick={submit}
                title={legal.blocked ? "Contract violates a CBA limit — see the flags above" : undefined}>
          <Icon.Check /> Sign player
        </button>
      </div>
    </Modal>
  );
}

/* ============================================================
   Add Player — for quick mock additions (trade-ins, two-ways, etc)
   ============================================================ */
function AddPlayerModal({ onClose, state, dispatch }) {
  const [name, setName] = useState("");
  const [salary, setSalary] = useState("");
  const [position, setPosition] = useState("G");
  const [source, setSource] = useState("Trade");

  function submit() {
    const n = parseSalaryFromMField(salary);
    if (!name.trim() || !n) return;
    dispatch({ type: "ADD_PLAYER", player: {
      id: "add-" + Date.now(),
      name: name.trim(), salary: n, years: 1, position, source,
    }});
    dispatch({ type: "SET_TOAST", toast: `Added ${name.trim()}` });
    onClose();
  }

  return (
    <Modal onClose={onClose}>
      <div className="modal-head">
        <div>
          <h3>Add a player to the books</h3>
          <div className="desc">For trade-ins, two-way conversions, or quick mocks.</div>
        </div>
      </div>
      <div className="modal-body">
        <div className="form-row">
          <label>Name</label>
          <input type="text" value={name} onChange={(e) => setName(e.target.value)} autoFocus />
        </div>
        <div className="form-grid">
          <div className="form-row">
            <label>2026-27 salary</label>
            <input type="text" value={salary} onChange={(e) => setSalary(e.target.value)} placeholder="e.g. 12.5M" />
          </div>
          <div className="form-row">
            <label>Position</label>
            <select value={position} onChange={(e) => setPosition(e.target.value)}>
              {["G","PG","SG","F","SF","PF","C"].map(p => <option key={p}>{p}</option>)}
            </select>
          </div>
        </div>
        <div className="form-row">
          <label>How acquired</label>
          <select value={source} onChange={(e) => setSource(e.target.value)}>
            <option>Trade</option>
            <option>Waiver claim</option>
            <option>10-day</option>
            <option>Two-way → standard</option>
            <option>Manual entry</option>
          </select>
        </div>
      </div>
      <div className="modal-foot">
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" disabled={!name.trim() || !salary} onClick={submit}>
          <Icon.Plus /> Add
        </button>
      </div>
    </Modal>
  );
}

/* ============================================================
   Trade Machine — your team on the left, picked team on the right.

   Real data: opponent rosters come from data/teams-trade-data.json
   (the V1 reconciled capsheets authority — all 30 teams, exact
   2026-27 figures, status + no-trade flags). Legality is the real
   V1 CBA engine (evaluateTeamSide / tradeMaxIncoming) in
   trade-engine.jsx — Expanded/Room TPE, 2nd-apron no-aggregation,
   NTC, apron + hard-cap tripwires.
   ============================================================ */

function tmTag(status) {
  switch (status) {
    case "team_option":    return "team option";
    case "player_option":  return "player option";
    case "partially_guaranteed": return "non-guar";
    case "non_guaranteed": return "non-guar";
    case "two_way":        return "two-way";
    default:               return null; // "guaranteed" → no tag
  }
}

function TradeMachineModal({ onClose, players, state, dispatch, derived, tradeData, teams = [] }) {
  const myCode = state.team || "LAL";
  const teamObj = (code) => teams.find(t => t.code === code) || { code, name: code, logo: "" };
  const newSlot = (code) => ({ code, players: new Map(), picks: new Map(),
                               swaps: [], cash: { out: 0, dest: null } });
  const [slots, setSlots] = useState([newSlot(myCode)]);
  const Y0 = new Date().getFullYear();
  const [adding, setAdding] = useState(false);
  const [activeSlot, setActiveSlot] = useState(0);
  const codes = slots.map(s => s.code);
  const canAdd = slots.length < 6;
  const multi = slots.length > 2;

  function rosterOf(code, i) {
    if (i === 0) {
      return players
        .filter(p => p.offseasonStatus !== "draft_pick" && p.offseasonStatus !== "UFA"
                  && p.offseasonStatus !== "RFA"
                  && playerEffectiveSalary(p, state.decisions, state.mode) > 0)
        .map(p => ({ name: p.name, salary: playerEffectiveSalary(p, state.decisions, state.mode),
                     photoId: p.nbaId, noTrade: !!p.noTrade,
                     tag: p.offseasonStatus === "under_contract" ? null : tmTag(p.offseasonStatus) }))
        .sort((a, b) => b.salary - a.salary);
    }
    const td = tradeData && tradeData.teams ? tradeData.teams[code] : null;
    return (td?.players || [])
      .filter(p => p.status !== "draft_pick")
      .map(p => ({ name: p.name, salary: p.salary2026_27 || 0, photoId: p.nbaId,
                   noTrade: !!p.noTrade, tag: tmTag(p.status) }))
      .sort((a, b) => b.salary - a.salary);
  }
  function picksOf(code, i) {
    let real;
    if (i === 0) {
      real = state.draftPicks.map(dp => ({
        id: dp.id, label: dp.name, sub: dp.pickNumber ? `#${dp.pickNumber} · this year` : "This year",
        photo: dp.prospectPhoto || null, managed: true }));
    } else {
      const td = tradeData && tradeData.teams ? tradeData.teams[code] : null;
      real = (td?.players || []).filter(p => p.status === "draft_pick")
        .map(p => ({ id: code + "-" + p.name.replace(/\W+/g, ""), label: p.name,
                     sub: "This year", photo: null, managed: false }));
    }
    const future = getPlaceholderPicks(code).map(fp => ({
      id: fp.id, label: fp.label, sub: "Future · placeholder", photo: null, managed: false, future: true }));
    return [...real, ...future];
  }
  const rosters = slots.map((s, i) => rosterOf(s.code, i));
  const slotPicks = slots.map((s, i) => picksOf(s.code, i));

  const defaultDest = (slotCode) => (codes.find(c => c !== slotCode) || null);
  function patchSlot(i, fn) { setSlots(prev => prev.map((s, idx) => idx === i ? fn(s) : s)); }
  function togglePlayer(i, name) {
    patchSlot(i, s => { const m = new Map(s.players);
      if (m.has(name)) m.delete(name); else m.set(name, { dest: defaultDest(s.code) });
      return { ...s, players: m }; });
  }
  function togglePick(i, id, managed) {
    patchSlot(i, s => { const m = new Map(s.picks);
      if (m.has(id)) m.delete(id);
      else m.set(id, { dest: defaultDest(s.code), mode: managed ? "trade-pick" : null });
      return { ...s, picks: m }; });
  }
  function setPickMode(i, id, mode) {
    patchSlot(i, s => { const m = new Map(s.picks);
      if (m.has(id)) m.set(id, { ...m.get(id), mode });
      return { ...s, picks: m }; });
  }
  function cycle(i, kind, key) {
    patchSlot(i, s => { const m = new Map(s[kind]); const cur = m.get(key);
      if (!cur) return s;
      m.set(key, { ...cur, dest: cycleDestination(cur.dest, codes, s.code) });
      return { ...s, [kind]: m }; });
  }
  function addTeam(code) {
    if (!canAdd || codes.includes(code)) { setAdding(false); return; }
    setSlots(prev => [...prev, newSlot(code)]);
    setAdding(false);
  }
  function removeTeam(i) {
    if (i === 0) return;
    setSlots(prev => {
      const next = prev.filter((_, idx) => idx !== i);
      const live = next.map(s => s.code);
      const fix = (mp, ownCode) => { const m = new Map();
        for (const [k, v] of mp) m.set(k, live.includes(v.dest) ? v
          : { ...v, dest: (live.find(c => c !== ownCode) || null) });
        return m; };
      return next.map(s => ({
        ...s,
        players: fix(s.players, s.code),
        picks: fix(s.picks, s.code),
        swaps: (s.swaps || []).filter(sw => live.includes(sw.swapWith)),
        cash: { out: s.cash?.out || 0,
                dest: live.includes(s.cash?.dest) ? s.cash.dest
                      : (live.find(c => c !== s.code) || null) },
      }));
    });
    setActiveSlot(0);
  }
  function clearAll() {
    setSlots(prev => prev.map(s => ({ ...s, players: new Map(), picks: new Map(),
      swaps: [], cash: { out: 0, dest: null } })));
  }
  function addSwap(i) {
    patchSlot(i, s => ({ ...s, swaps: [...(s.swaps || []), {
      id: "sw-" + Date.now(), year: Y0 + 1, round: 1,
      swapWith: codes.find(c => c !== s.code) || null, targetPickId: null }] }));
  }
  function setSwap(i, id, patch) {
    patchSlot(i, s => ({ ...s, swaps: (s.swaps || []).map(sw =>
      sw.id === id ? { ...sw, ...patch } : sw) }));
  }
  function removeSwap(i, id) {
    patchSlot(i, s => ({ ...s, swaps: (s.swaps || []).filter(sw => sw.id !== id) }));
  }
  function setCash(i, patch) {
    patchSlot(i, s => ({ ...s, cash: { ...(s.cash || { out: 0, dest: null }), ...patch } }));
  }
  function cycleCashDest(i) {
    patchSlot(i, s => ({ ...s, cash: { ...(s.cash || { out: 0 }),
      dest: cycleDestination(s.cash?.dest, codes, s.code) || defaultDest(s.code) } }));
  }
  // Swap targets for (swapWith, year, round): that team's owned future
  // picks + picks arriving to it via this trade.
  function swapTargets(swapWith, year, round) {
    const si = codes.indexOf(swapWith);
    if (si < 0) return [];
    const arriving = [];
    slots.forEach((o, oi) => {
      if (o.code === swapWith) return;
      for (const [pid, meta] of o.picks) if (meta.dest === swapWith) {
        const pk = slotPicks[oi].find(p => p.id === pid);
        if (pk && pk.year) arriving.push({ pick: pk, from: o.code });
      }
    });
    return getAvailableSwapTargets(year, round,
      [{ code: swapWith, owned: slotPicks[si].filter(p => p.year), arriving }]);
  }

  const sideOf = (s, i) => {
    const out = [...s.players.keys()].map(n => rosters[i].find(p => p.name === n)).filter(Boolean);
    const inn = [];
    slots.forEach((o, oi) => {
      if (oi === i) return;
      for (const [n, meta] of o.players) if (meta.dest === s.code) {
        const pp = rosters[oi].find(p => p.name === n); if (pp) inn.push(pp);
      }
    });
    const t = teamObj(s.code);
    const preSalary = i === 0 ? ((derived && derived.committed) || 0)
                              : ((t && t.total) || rosters[i].reduce((a, p) => a + p.salary, 0));
    return { code: s.code, out, in: inn, preSalary };
  };
  const sides = slots.map((s, i) => sideOf(s, i));
  const anyActivity = sides.some(sd => sd.out.length || sd.in.length)
    || slots.some(s => s.picks.size);
  const evals = sides.map(sd => (sd.out.length || sd.in.length) ? evaluateTeamSide(sd) : null);
  const legal = anyActivity && evals.every(e => !e || e.legal) && evals.some(e => e && e.legal);

  function apply() {
    slots.forEach((o, oi) => {
      if (oi === 0) return;
      for (const [n, meta] of o.players) if (meta.dest === myCode) {
        const pp = rosters[oi].find(p => p.name === n);
        if (pp) dispatch({ type: "ADD_PLAYER", player: {
          id: "trd-" + o.code + "-" + n.replace(/\W+/g, ""),
          name: n, salary: pp.salary, years: 1, source: `Trade · ${o.code}`, position: "" } });
      }
    });
    for (const n of slots[0].players.keys())
      dispatch({ type: "SET_DECISION", player: n, decision: { kind: "traded" } });
    for (const [id, meta] of slots[0].picks) {
      if (state.draftPicks.some(dp => dp.id === id))
        dispatch({ type: "SET_DRAFT_PICK", id,
          patch: { disposition: meta.mode === "sign-and-trade" ? "sign-and-trade" : "trade-pick" } });
    }
    const outN = slots[0].players.size + slots[0].picks.size;
    const inN = slots.reduce((a, o, oi) => a + (oi === 0 ? 0
      : [...o.players.values()].filter(v => v.dest === myCode).length), 0);
    dispatch({ type: "SET_TOAST", toast: `Trade applied: ${outN} out, ${inN} in (${slots.length} teams).` });
    onClose();
  }

  return (
    <Modal onClose={onClose} wide>
      <div className="modal-head" style={{ paddingBottom: 12 }}>
        <Icon.Trade style={{ width: 18, height: 18, color: "var(--brand)" }} />
        <div>
          <h3>Trade Machine</h3>
          <div className="desc">2–6 teams. Click players/picks to include them; in 3+ team trades tap the
            destination logo to route. Real CBA salary-matching per team.</div>
        </div>
      </div>

      <div className="tm-tabs">
        {slots.map((s, i) => (
          <button key={s.code} className={`tm-tab ${i === activeSlot ? "on" : ""}`}
                  onClick={() => setActiveSlot(i)}>
            <span className="logo"><TeamLogo team={teamObj(s.code)} size={18} /></span>{s.code}
          </button>
        ))}
        {canAdd && (
          <button className={`tm-tab add ${adding ? "on" : ""}`}
                  onClick={() => { setAdding(true); setActiveSlot(slots.length); }}>
            <Icon.Plus /> Team
          </button>
        )}
      </div>

      <div className="tm">
        {slots.map((s, i) => (
          <TmTeamColumn key={s.code} idx={i} slot={s} team={teamObj(s.code)} teamOf={teamObj}
            pinned={i === 0} active={i === activeSlot} multi={multi} codes={codes}
            roster={rosters[i]} picks={slotPicks[i]} side={sides[i]} ev={evals[i]}
            onTogglePlayer={(n) => togglePlayer(i, n)}
            onTogglePick={(id, mg) => togglePick(i, id, mg)}
            onSetPickMode={(id, m) => setPickMode(i, id, m)}
            onCyclePlayer={(n) => cycle(i, "players", n)}
            onCyclePick={(id) => cycle(i, "picks", id)}
            onAddSwap={() => addSwap(i)}
            onSetSwap={(id, p) => setSwap(i, id, p)}
            onRemoveSwap={(id) => removeSwap(i, id)}
            swapTargets={swapTargets}
            onSetCash={(p) => setCash(i, p)}
            onCycleCashDest={() => cycleCashDest(i)}
            cashLimit={cashLimit()}
            onRemove={i === 0 ? null : () => removeTeam(i)} />
        ))}
        {(adding || slots.length === 1) && canAdd && (
          <div className={`tm-column tm-add ${activeSlot === slots.length ? "active" : ""}`}>
            <div className="tm-column-head"><span style={{ fontWeight: 700 }}>Add a team</span></div>
            <TmTeamPicker currentTeam={{ code: "" }}
                          teams={teams.filter(t => !codes.includes(t.code))}
                          onPick={(t) => addTeam(t.code)} />
          </div>
        )}
        {canAdd && !adding && slots.length > 1 && (
          <button className="tm-add-slot" onClick={() => { setAdding(true); setActiveSlot(slots.length); }}>
            <Icon.Plus /><span>Add team</span>
          </button>
        )}
      </div>

      {anyActivity && (
        <div className="tm-verdict">
          <div className={`tm-verdict-head ${legal ? "legal" : "illegal"}`}>
            {legal ? <Icon.Check className="ico" /> : <Icon.X className="ico" />}
            {legal ? "Trade is legal" : "Trade is NOT legal"}
          </div>
          <div className="tm-verdict-body">
            {sides.map((sd, i) => evals[i] && (
              <div key={sd.code}>
                <div className={`line ${evals[i].legal ? "ok" : "bad"}`}>
                  <b>{sd.code}</b> sends {fmt$(sd.out.reduce((a, p) => a + p.salary, 0))} · takes {fmt$(sd.in.reduce((a, p) => a + p.salary, 0))}
                </div>
                {evals[i].reasons.map((m, k) => (
                  <div key={k} className={`line ${evals[i].legal ? "ok" : "bad"}`}>{m}</div>
                ))}
                {evals[i].flags.map((m, k) => (
                  <div key={"f" + k} className="line" style={{ color: "var(--warn)" }}>⚠ {m}</div>
                ))}
              </div>
            ))}
            <div className="line" style={{ color: "var(--text-faint)" }}>
              v1 CBA core · picks/cash not in salary match · swaps &amp; cash deferred (Phase 2)
            </div>
          </div>
        </div>
      )}

      <div className="modal-foot">
        <button className="btn btn-ghost" onClick={clearAll}>Clear trade</button>
        <button className="btn" onClick={onClose}>Close</button>
        <button className="btn btn-primary" disabled={!legal} onClick={apply}>
          <Icon.Check /> Apply trade
        </button>
      </div>
    </Modal>
  );
}

function TmTeamColumn({ idx, slot, team, teamOf, pinned, active, multi, codes, roster, picks, side, ev,
                        onTogglePlayer, onTogglePick, onSetPickMode, onCyclePlayer, onCyclePick,
                        onAddSwap, onSetSwap, onRemoveSwap, swapTargets,
                        onSetCash, onCycleCashDest, cashLimit, onRemove }) {
  const sends = side.out.reduce((a, p) => a + p.salary, 0);
  const gets = side.in.reduce((a, p) => a + p.salary, 0);
  const cashOut = slot.cash?.out || 0;
  const swaps = slot.swaps || [];
  const others = codes.filter(c => c !== slot.code);
  const normalPicks = picks.filter(p => !p.isResidual);
  const residualPicks = picks.filter(p => p.isResidual);

  const renderPick = (pk) => {
    const sel = slot.picks.get(pk.id);
    return (
      <div key={pk.id}>
        <div className={`tm-row tm-pick-row ${sel ? "picked" : ""} ${pk.future ? "future" : ""} ${pk.isResidual ? "residual" : ""}`}
             title={pk.residualNote || undefined}
             onClick={() => onTogglePick(pk.id, !!pk.managed)}>
          {sel && multi && (
            <span className="tm-dest" title="Change destination"
                  onClick={(e) => { e.stopPropagation(); onCyclePick(pk.id); }}>
              <TeamLogo team={teamOf(sel.dest)} size={16} /><span className="arr">→</span>
            </span>
          )}
          {pk.photo
            ? <img className="photo" src={pk.photo} onError={(e) => e.currentTarget.remove()} alt="" />
            : <div className="photo ph">{pk.isResidual ? "⚠" : "PK"}</div>}
          <div className="name">
            {sel ? "✓ " : ""}<b>{pk.label}</b>
            <span className="tag">{pk.sub || (pk.isResidual ? "residual" : "")}</span>
          </div>
        </div>
        {sel && pk.managed && (
          <div className="tm-pick-mode">
            <div className="seg">
              <button className={sel.mode !== "sign-and-trade" ? "on" : ""}
                      onClick={() => onSetPickMode(pk.id, "trade-pick")}>Trade the pick</button>
              <button className={sel.mode === "sign-and-trade" ? "on" : ""}
                      onClick={() => onSetPickMode(pk.id, "sign-and-trade")}>Sign &amp; trade</button>
            </div>
            {sel.mode === "sign-and-trade" && (
              <div className="alert alert-warn" role="status">
                <span className="alert-ico">⚠</span>
                <div className="alert-body">Sign-and-trade has a <b>30-day wait</b> between
                  signing and the trade — normal trades are instant.</div>
              </div>
            )}
          </div>
        )}
      </div>
    );
  };

  return (
    <div className={`tm-column ${pinned ? "pinned" : ""} ${active ? "active" : ""}`}>
      <div className="tm-column-head">
        <span className="logo"><TeamLogo team={team} size={22} /></span>
        <span className="team-name">{slot.code}</span>
        {pinned && <span className="tm-you">you</span>}
        {onRemove && <button className="tm-remove" title="Remove team" onClick={onRemove}><Icon.X /></button>}
      </div>
      <div className="tm-flow">
        <div>↑ {fmt$(sends + cashOut)} <span className="dim">{side.out.length}p
          {slot.picks.size ? " · " + slot.picks.size + "pk" : ""}
          {swaps.length ? " · " + swaps.length + "sw" : ""}
          {cashOut ? " · cash" : ""}</span></div>
        <div>↓ {fmt$(gets)} <span className="dim">{side.in.length}p</span></div>
      </div>

      <div className="tm-list">
        {roster.length === 0
          ? <div className="empty">No roster data.</div>
          : roster.map(p => {
              const sel = slot.players.get(p.name);
              return (
                <div key={p.name} className={`tm-row ${sel ? "picked" : ""}`}
                     onClick={() => onTogglePlayer(p.name)}>
                  {sel && multi && (
                    <span className="tm-dest" title="Click to change destination"
                          onClick={(e) => { e.stopPropagation(); onCyclePlayer(p.name); }}>
                      <TeamLogo team={teamOf(sel.dest)} size={16} /><span className="arr">→</span>
                    </span>
                  )}
                  {p.photoId
                    ? <img className="photo" src={`assets/players/${p.photoId}.png`} onError={(e) => e.currentTarget.remove()} alt="" />
                    : <div className="photo ph">{p.name.split(" ").map(s => s[0]).slice(0, 2).join("")}</div>}
                  <div className="name">
                    {sel ? "✓ " : ""}<b>{p.name}</b>
                    {p.noTrade && <span className="tag" title="No-trade clause">NTC</span>}
                    {p.tag && <span className="tag">{p.tag}</span>}
                  </div>
                  <span className="salary num">{fmt$(p.salary)}</span>
                </div>
              );
            })}
      </div>

      {picks.length > 0 && (
        <div className="tm-picks">
          <div className="tm-picks-head">DRAFT PICKS</div>
          {normalPicks.map(renderPick)}
          {residualPicks.length > 0 && (
            <>
              <div className="tm-picks-divider">CONDITIONAL / RESIDUAL</div>
              {residualPicks.map(renderPick)}
            </>
          )}
        </div>
      )}

      {others.length > 0 && (
        <div className="tm-swaps">
          <div className="tm-picks-head">PICK SWAPS</div>
          {swaps.map(sw => {
            const tgts = swapTargets(sw.swapWith, sw.year, sw.round);
            return (
              <div key={sw.id} className="tm-swap-row">
                <div className="tm-swap-line">
                  🔄 <b>{slot.code}</b> owns the right to swap
                </div>
                <div className="tm-swap-cfg">
                  <select value={sw.year} onChange={(e) => onSetSwap(sw.id, { year: +e.target.value, targetPickId: null })}>
                    {Array.from({ length: 6 }).map((_, k) => {
                      const y = (new Date().getFullYear()) + 1 + k;
                      return <option key={y} value={y}>{y}</option>;
                    })}
                  </select>
                  <select value={sw.round} onChange={(e) => onSetSwap(sw.id, { round: +e.target.value, targetPickId: null })}>
                    <option value={1}>1st</option><option value={2}>2nd</option>
                  </select>
                  <span className="tm-swap-with">with</span>
                  <select value={sw.swapWith || ""} onChange={(e) => onSetSwap(sw.id, { swapWith: e.target.value, targetPickId: null })}>
                    {others.map(c => <option key={c} value={c}>{c}</option>)}
                  </select>
                  <button className="tm-remove" title="Remove swap" onClick={() => onRemoveSwap(sw.id)}><Icon.X /></button>
                </div>
                <div className="tm-swap-targets">
                  {tgts.length === 0
                    ? <span className="dim">{sw.swapWith || "team"} has no {sw.year} {sw.round === 1 ? "1st" : "2nd"} to target.</span>
                    : tgts.map(t => (
                        <label key={t.id} className="tm-swap-tgt">
                          <input type="radio" name={sw.id} checked={sw.targetPickId === t.id}
                                 onChange={() => onSetSwap(sw.id, { targetPickId: t.id })} />
                          <span>{t.label}{t.isArriving && <span className="tm-arriving" title={"Arriving from " + t.arrivingFrom + " via this trade"}> ★ via {t.arrivingFrom}</span>}</span>
                        </label>
                      ))}
                </div>
              </div>
            );
          })}
          <button className="btn btn-ghost tm-add-swap" onClick={onAddSwap}>
            <Icon.Plus /> Add swap
          </button>
        </div>
      )}

      {others.length > 0 && (
        <div className="tm-cash">
          <div className="tm-picks-head">CASH</div>
          <div className="tm-cash-row">
            <span>Send</span>
            <input type="text" className="tm-cash-input" value={cashOut ? cashOut : ""}
                   placeholder="0" onChange={(e) => onSetCash({ out: parseSalaryInput(e.target.value) || 0 })} />
            {cashOut > 0 && multi && (
              <span className="tm-dest" title="Change destination"
                    onClick={onCycleCashDest}>
                <TeamLogo team={teamOf(slot.cash?.dest)} size={16} /><span className="arr">→</span>
              </span>
            )}
          </div>
          <div className="tm-cash-note">CBA: up to {fmt$(cashLimit)} sent / received per season.</div>
        </div>
      )}
    </div>
  );
}

/* Team picker shown in-place on the right side of the trade machine.
   2ways-style: West on left, East on right, ordered by rank, no scroll. */
function TmTeamPicker({ currentTeam, teams = [], onPick }) {
  const westByRank = {};
  const eastByRank = {};
  for (const t of teams) {
    (t.conf === "W" ? westByRank : eastByRank)[t.rank] = t;
  }
  const maxRows = 15;
  return (
    <div className="tm-picker">
      <div className="tm-picker-head">
        <span>WEST</span>
        <span></span>
        <span>EAST</span>
      </div>
      {Array.from({ length: maxRows }).map((_, i) => {
        const w = westByRank[i + 1];
        const e = eastByRank[i + 1];
        return (
          <div className="tm-picker-row" key={i}>
            <button className={`tm-picker-cell west ${w?.code === currentTeam.code ? "current" : ""} ${w ? "" : "empty"}`}
                    disabled={!w || w.code === currentTeam.code}
                    onClick={() => w && w.code !== currentTeam.code && onPick(w)}>
              {w && (<>
                <span className="logo"><TeamLogo team={w} size={26} /></span>
                <span className="name">{shortTeam(w.name)}</span>
                {w.code === currentTeam.code && <span className="tm-curr">current</span>}
              </>)}
            </button>
            <span className="tm-picker-rank">{i + 1}</span>
            <button className={`tm-picker-cell east ${e?.code === currentTeam.code ? "current" : ""} ${e ? "" : "empty"}`}
                    disabled={!e || e.code === currentTeam.code}
                    onClick={() => e && e.code !== currentTeam.code && onPick(e)}>
              {e && (<>
                {e.code === currentTeam.code && <span className="tm-curr">current</span>}
                <span className="name">{shortTeam(e.name)}</span>
                <span className="logo"><TeamLogo team={e} size={26} /></span>
              </>)}
            </button>
          </div>
        );
      })}
    </div>
  );
}

/* ============================================================
   Draft Prospect picker — assign a real prospect (name + photo) to a
   draft-pick slot. Data: data/draft-prospects.json (ESPN best-available,
   scraped once). Selecting one patches the pick via SET_DRAFT_PICK.
   ============================================================ */
function DraftProspectModal({ onClose, pickId, dispatch }) {
  const [list, setList] = useState(null);
  const [q, setQ] = useState("");
  useEffect(() => {
    fetch("data/draft-prospects.json").then(r => r.json()).then(setList).catch(() => setList([]));
  }, []);
  const term = q.trim().toLowerCase();
  const shown = (list || []).filter(p =>
    !term || p.name.toLowerCase().includes(term) || (p.pos || "").toLowerCase() === term);

  function pick(p) {
    dispatch({ type: "SET_DRAFT_PICK", id: pickId, patch: { name: p.name, prospectPhoto: p.photo || null } });
    dispatch({ type: "SET_TOAST", toast: `Pencilled in ${p.name}.` });
    onClose();
  }

  return (
    <Modal onClose={onClose} wide>
      <div className="modal-head">
        <div>
          <h3>Choose a draft prospect</h3>
          <div className="desc">ESPN best available. To change which pick you hold, use the Trade Machine.</div>
        </div>
      </div>
      <div className="modal-body">
        <input className="prospect-search" type="text" autoFocus placeholder="Search name or position (G/F/C)…"
               value={q} onChange={(e) => setQ(e.target.value)} />
        {list == null
          ? <div className="empty">Loading prospects…</div>
          : shown.length === 0
          ? <div className="empty">No prospects match.</div>
          : <div className="prospect-grid">
              {shown.map(p => (
                <button key={p.id} className="prospect-card" onClick={() => pick(p)}>
                  {p.photo
                    ? <img className="prospect-photo" src={p.photo} alt="" loading="lazy" decoding="async"
                           onError={(e) => { e.currentTarget.style.display = "none"; e.currentTarget.nextSibling.style.display = "grid"; }} />
                    : null}
                  <div className="prospect-photo placeholder" style={{ display: p.photo ? "none" : "grid" }}>
                    {p.name.split(" ").map(s => s[0]).slice(0, 2).join("")}
                  </div>
                  <div className="prospect-meta">
                    <div className="prospect-name">{p.name}</div>
                    <div className="prospect-sub">#{p.rank} · {p.pos || "—"}</div>
                  </div>
                </button>
              ))}
            </div>}
      </div>
      <div className="modal-foot">
        <button className="btn" onClick={onClose}>Cancel</button>
      </div>
    </Modal>
  );
}

/* ============================================================
   G3 (e) — Snapshot: a shareable "offseason changes" card → PNG.
   Purpose-built layout captured by html2canvas (CDN). Portrait/landscape,
   Download + Copy. First of the snapshot modes (final-roster / per-trade follow).
   ============================================================ */
function SnapshotModal({ onClose, state, players, derived, teamName, teamCode }) {
  const [orient, setOrient] = useState("portrait");
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState("");
  const cardRef = useRef(null);
  const moves = (typeof computeManagedMoves === "function")
    ? computeManagedMoves(state, players)
    : { count: 0, signed: [], acquired: [], sent: [], reSigned: [], waived: [], renounced: [] };
  const committed = (derived && Number.isFinite(derived.committed)) ? derived.committed : 0;
  const room = CAP_2026.cap - committed;

  function flash(t) { setMsg(t); setTimeout(() => setMsg(""), 2000); }
  async function snap() {
    if (typeof html2canvas !== "function") { flash("Export library still loading…"); return null; }
    if (!cardRef.current) return null;
    return html2canvas(cardRef.current, { scale: 2, backgroundColor: null, useCORS: true, logging: false });
  }
  async function onDownload() {
    setBusy(true);
    try {
      const canvas = await snap();
      if (canvas) canvas.toBlob(blob => {
        if (!blob) { flash("Export failed"); return; }
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url; a.download = `capmvp-${(teamCode || "team").toLowerCase()}-changes.png`;
        document.body.appendChild(a); a.click(); a.remove();
        setTimeout(() => URL.revokeObjectURL(url), 2000);
        flash("Downloaded");
      }, "image/png");
    } catch (e) { flash("Export failed"); } finally { setBusy(false); }
  }
  async function onCopy() {
    setBusy(true);
    try {
      const canvas = await snap();
      if (canvas && navigator.clipboard && window.ClipboardItem) {
        canvas.toBlob(async blob => {
          try { await navigator.clipboard.write([new window.ClipboardItem({ "image/png": blob })]); flash("Copied to clipboard"); }
          catch (e) { flash("Copy blocked — use Download"); }
        }, "image/png");
      } else flash("Copy unsupported — use Download");
    } catch (e) { flash("Copy failed"); } finally { setBusy(false); }
  }

  function Group({ title, items, fmt }) {
    if (!items || !items.length) return null;
    return (
      <div className="snap-group">
        <div className="snap-group-h">{title}<span>{items.length}</span></div>
        {items.map((it, i) => <div className="snap-row" key={i}>{fmt(it)}</div>)}
      </div>
    );
  }

  return (
    <div className="scrim" onClick={onClose}>
      <div className="modal snapshot-modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <div><h3>Snapshot — changes</h3><div className="desc">Share your offseason moves as an image.</div></div>
        </div>
        <div className="snap-controls">
          <div className="sb-mode-toggle">
            <button className={orient === "portrait" ? "on" : ""} onClick={() => setOrient("portrait")}>Portrait</button>
            <button className={orient === "landscape" ? "on" : ""} onClick={() => setOrient("landscape")}>Landscape</button>
          </div>
          <span style={{ flex: 1 }} />
          {msg && <span className="snap-msg">{msg}</span>}
          <button className="btn" disabled={busy} onClick={onCopy}>Copy</button>
          <button className="btn btn-primary" disabled={busy} onClick={onDownload}>⬇ Download</button>
        </div>
        <div className="snap-stage">
          <div className={`snap-card ${orient}`} ref={cardRef}>
            <div className="snap-card-head">
              {typeof TeamLogo === "function" && <TeamLogo code={teamCode} size={38} />}
              <div className="snap-card-title">
                <div className="snap-team">{teamName || teamCode}</div>
                <div className="snap-sub">Offseason moves</div>
              </div>
              <div className="snap-brand">cap<b>MVP</b></div>
            </div>
            {moves.count === 0 ? (
              <div className="snap-empty">No moves yet — make a trade, sign a free agent, or waive a player.</div>
            ) : (
              <div className="snap-groups">
                <Group title="Signed" items={moves.signed} fmt={(it) => <><span className="nm">{it.name}</span><span className="meta">{fmt$(it.salary)}{it.years > 1 ? ` · ${it.years}y` : ""}</span></>} />
                <Group title="Acquired" items={moves.acquired} fmt={(it) => <><span className="nm">{it.name}</span><span className="meta">{it.from ? `from ${it.from}` : ""}{it.salary ? ` · ${fmt$(it.salary)}` : ""}</span></>} />
                <Group title="Re-signed" items={moves.reSigned} fmt={(it) => <><span className="nm">{it.name}</span><span className="meta">{fmt$(it.salary)}{it.years > 1 ? ` · ${it.years}y` : ""}</span></>} />
                <Group title="Traded away" items={moves.sent} fmt={(it) => <><span className="nm">{it.name}</span><span className="meta">{it.to ? `to ${it.to}` : ""}{it.snt ? " · S&T" : ""}</span></>} />
                <Group title="Waived" items={moves.waived} fmt={(it) => <><span className="nm">{it.name}</span><span className="meta">{it.label}{it.dead ? ` · ${fmt$(it.dead)}` : ""}</span></>} />
                <Group title="Renounced" items={moves.renounced} fmt={(it) => <><span className="nm">{it.name}</span></>} />
              </div>
            )}
            <div className="snap-card-foot">
              <span>Payroll <b>{fmt$(committed)}</b></span>
              <span className={room >= 0 ? "pos" : "neg"}>{room >= 0 ? `${fmt$(room)} cap room` : `${fmt$(-room)} over cap`}</span>
            </div>
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Close</button>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   Trade history / undo window (B/C/H) — lists every applied trade
   (latest first); each can be undone. UNDO_TRADE reverses the whole
   trade for ALL teams and restores each player's pre-trade decision
   (state.jsx reverseTradeInRosters). Opened via a window event so the
   per-player ↺ (focused on its trade) and the header "Trades" button
   both reach it with no prop-drilling. focusId highlights one trade.
   ============================================================ */
function TradeHistoryModal({ onClose, focusId, state, dispatch, teams }) {
  const scen = state && state.scenarios && state.activeScenario && state.scenarios[state.activeScenario];
  const applied = (scen && scen.appliedTrades) || (state && state.appliedTrades) || [];
  const focusRef = useRef(null);
  useEffect(() => { if (focusRef.current) focusRef.current.scrollIntoView({ block: "nearest" }); }, [focusId, applied.length]);
  const [shareTrade, setShareTrade] = useState(null);
  const nameOf = (code) => { const t = (teams || []).find(x => x.code === code); return (t && (t.shortName || t.name)) || code; };
  const outgoing = (sl) => {
    const out = [];
    for (const p of (sl.outPlayers || [])) out.push(p.name);
    for (const f of (sl.outFas || [])) out.push(`${f.name} (S&T)`);
    for (const pk of (sl.outPicks || [])) out.push(pk.label || pk.id || "pick");
    if (sl.cashOut > 0) out.push(`${fmt$(sl.cashOut)} cash`);
    return out;
  };
  return (
    <>
    <Modal onClose={onClose} wide>
      <div className="modal-head">
        <div>
          <h3>Applied trades</h3>
          <div className="desc">
            {applied.length
              ? `${applied.length} applied in this scenario. Undo reverses the whole trade for every team and restores pre-trade decisions.`
              : "No trades applied yet."}
          </div>
        </div>
      </div>
      {/* the per-trade Share button opens this snapshot over the list */}
      <div className="modal-body">
        {applied.length === 0 && (
          <div className="th-empty">No applied trades. Build one in the Trade Machine and it will appear here.</div>
        )}
        {[...applied].reverse().map((tr) => {
          const isLatest = applied[applied.length - 1] === tr;
          const isFocus = !!focusId && tr.id === focusId;
          return (
            <div key={tr.id} ref={isFocus ? focusRef : null} className={`th-card ${isFocus ? "focus" : ""}`}>
              <div className="th-card-head">
                <span className="th-teams">{(tr.slots || []).map(s => s.code).join("  ↔  ")}</span>
                {isLatest && <span className="th-latest">latest</span>}
                <span style={{ flex: 1 }} />
                <button className="btn th-share"
                        onClick={() => setShareTrade(tr)}
                        title="Share this trade as an image">
                  Share
                </button>
                <button className="btn th-undo"
                        onClick={() => dispatch({ type: "UNDO_TRADE", tradeId: tr.id })}
                        title="Undo this trade — reverses it for every team and restores pre-trade decisions">
                  ↺ Undo trade
                </button>
              </div>
              <div className="th-slots">
                {(tr.slots || []).map((sl) => {
                  const out = outgoing(sl);
                  return (
                    <div key={sl.code} className="th-slot">
                      <span className="th-slot-team">{nameOf(sl.code)} <span className="th-code">{sl.code}</span></span>
                      <span className="th-arrow">sends</span>
                      <span className="th-slot-assets">
                        {out.length
                          ? out.map((a, j) => <span key={j} className="th-chip">{a}</span>)
                          : <span className="th-chip th-none">nothing</span>}
                      </span>
                    </div>
                  );
                })}
              </div>
            </div>
          );
        })}
      </div>
      <div className="modal-foot">
        <button className="btn" onClick={onClose}>Close</button>
      </div>
    </Modal>
    {shareTrade && <TradeSnapshotModal trade={shareTrade} teams={teams} onClose={() => setShareTrade(null)} />}
    </>
  );
}

/* ============================================================
   Trade snapshot — shareable PNG of ONE trade (the "card-per-team"
   layout chosen in the design competition). Built from an applied
   trade record; captured by html2canvas. Opened from the
   Transactions window's per-trade Share button. SENDS = compact
   rows, GETS = cards; Net Salary counts PLAYER salaries only
   (cash + picks are not salary).
   ============================================================ */
const TS_TEAM_COLORS = {
  ATL:"#E03A3E", BOS:"#1A7A3E", BKN:"#2e2e2e", CHA:"#2A1A6B", CHI:"#CE1141",
  CLE:"#8B0038", DAL:"#0B6CB8", DEN:"#1B3A6B", DET:"#C8102E", GSW:"#1D5BC4",
  HOU:"#CE1141", IND:"#0B2C6B", LAC:"#C8102E", LAL:"#552583", MEM:"#5D76A9",
  MIA:"#B0285A", MIL:"#0E5C2C", MIN:"#1B4B8A", NOP:"#1B2C5A", NYK:"#1574C8",
  OKC:"#1B86C9", ORL:"#0B7DC4", PHI:"#1574C8", PHX:"#3A1A7A", POR:"#C8333A",
  SAC:"#6A3A9B", SAS:"#5A6472", TOR:"#C8102E", UTA:"#0A3A6B", WAS:"#1B2C5A",
};
function tsColor(code) { return TS_TEAM_COLORS[code] || "#3a4150"; }
function tsInitials(name) { return (name || "").split(" ").filter(Boolean).map(s => s[0]).slice(0, 2).join("").toUpperCase(); }

function TradeSnapshotModal({ onClose, trade, teams = [] }) {
  const [busy, setBusy] = useState(false);
  const [msg, setMsg] = useState("");
  const cardRef = useRef(null);
  function flash(t) { setMsg(t); setTimeout(() => setMsg(""), 2200); }

  const teamBy = {}; for (const t of (teams || [])) teamBy[t.code] = t;
  const teamName = (code) => (teamBy[code] && teamBy[code].name) || code;
  const slots = (trade && Array.isArray(trade.slots)) ? trade.slots : [];
  const n = slots.length;

  // outPicks carry only {id,destCode}; the matching INCOMING pick has the label.
  const pickLabel = {};
  for (const sl of slots) for (const inc of (sl.incoming || []))
    if (inc.kind === "pick" && inc.id) pickLabel[inc.id] = inc.label || (inc.year ? `${inc.year} ${inc.round === 1 ? "1st" : "2nd"}` : "Draft pick");

  function itemsFor(sl) {
    const sends = [];
    for (const p of (sl.outPlayers || [])) { const sp = p.sourcePlayer || {}; sends.push({ kind: "player", name: p.name, pos: sp.position, amt: fmt$(sp.salary || 0), sal: (sp.salary || 0), route: p.destCode }); }
    for (const f of (sl.outFas || [])) { const sp = f.sourcePlayer || {}; sends.push({ kind: "player", name: f.name, pos: sp.position || sp.pos, amt: fmt$(f.sntSalary || 0), sal: (f.sntSalary || 0), route: f.destCode }); }
    for (const pk of (sl.outPicks || [])) sends.push({ kind: "pick", name: pickLabel[pk.id] || "Draft pick", amt: "", sal: 0, route: pk.destCode });
    if (sl.cashOut > 0) sends.push({ kind: "cash", name: "Cash", amt: fmt$(sl.cashOut), sal: 0, route: sl.cashDest });
    const gets = [];
    for (const inc of (sl.incoming || [])) {
      if (inc.kind === "player" || inc.kind === "fa") gets.push({ kind: "player", name: inc.name, pos: inc.position, amt: fmt$(inc.salary || 0), sal: (inc.salary || 0), route: inc.fromCode });
      else if (inc.kind === "pick") gets.push({ kind: "pick", name: inc.label || (inc.year ? `${inc.year} ${inc.round === 1 ? "1st" : "2nd"}` : "Draft pick"), amt: "Pick", sal: 0, route: inc.fromCode });
      else if (inc.kind === "cash") gets.push({ kind: "cash", name: "Cash", amt: fmt$(inc.amount || 0), sal: 0, route: inc.fromCode });
    }
    const net = gets.reduce((a, x) => a + x.sal, 0) - sends.reduce((a, x) => a + x.sal, 0);
    return { sends, gets, net };
  }

  function Ini({ x, sm }) {
    const cls = "ts-ini " + (sm ? "sm" : "lg");
    if (x.kind === "pick") return <span className={cls + " pick"}>&#9733;</span>;
    if (x.kind === "cash") return <span className={cls + " cash"}>$</span>;
    return <span className={cls}>{tsInitials(x.name)}</span>;
  }
  const Dot = ({ code }) => <span className="ts-dot" style={{ background: tsColor(code) }} />;

  async function snap() {
    if (typeof html2canvas !== "function") { flash("Export library still loading…"); return null; }
    if (!cardRef.current) return null;
    return html2canvas(cardRef.current, { scale: 2, backgroundColor: null, useCORS: true, logging: false });
  }
  async function onDownload() {
    setBusy(true);
    try {
      const c = await snap();
      if (c) c.toBlob(b => {
        if (!b) { flash("Export failed"); return; }
        const u = URL.createObjectURL(b); const a = document.createElement("a");
        a.href = u; a.download = `capmvp-trade-${(trade && trade.id) || "deal"}.png`;
        document.body.appendChild(a); a.click(); a.remove();
        setTimeout(() => URL.revokeObjectURL(u), 2000); flash("Downloaded");
      }, "image/png");
    } catch (e) { flash("Export failed"); } finally { setBusy(false); }
  }
  async function onCopy() {
    setBusy(true);
    try {
      const c = await snap();
      if (c && navigator.clipboard && window.ClipboardItem) c.toBlob(async b => {
        try { await navigator.clipboard.write([new window.ClipboardItem({ "image/png": b })]); flash("Copied to clipboard"); }
        catch (e) { flash("Copy blocked — use Download"); }
      }, "image/png");
      else flash("Copy unsupported — use Download");
    } catch (e) { flash("Copy failed"); } finally { setBusy(false); }
  }

  const cols = Math.min(4, Math.max(2, n));
  return (
    <div className="scrim" onClick={onClose}>
      <div className="modal snapshot-modal ts-modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head"><div><h3>Trade snapshot</h3><div className="desc">Share this trade as an image.</div></div></div>
        <div className="snap-controls">
          <span style={{ flex: 1 }} />
          {msg && <span className="snap-msg">{msg}</span>}
          <button className="btn" disabled={busy} onClick={onCopy}>Copy</button>
          <button className="btn btn-primary" disabled={busy} onClick={onDownload}>&#8595; Download</button>
        </div>
        <div className="snap-stage">
          <div className="ts-scroll">
            <div className="ts-card" ref={cardRef}>
              <div className="ts-top">
                <div className="ts-brand"><span className="cap">cap</span><span className="mvp">MVP</span><span className="ts-tag">Trade Machine</span></div>
                <div className="ts-topright"><span className="ts-deal"><span className="n">{n}</span>-Team Trade</span><span className="ts-season">2026-27</span></div>
              </div>
              <div className={`ts-grid cols-${cols}`}>
                {slots.map((sl, i) => {
                  const { sends, gets, net } = itemsFor(sl);
                  const tc = tsColor(sl.code);
                  return (
                    <div className="ts-tcard" key={sl.code + i}>
                      <div className="ts-thead" style={{ background: `linear-gradient(180deg, rgba(255,255,255,.06), rgba(0,0,0,.20)), ${tc}` }}>
                        <span className="ts-chip">{sl.code}</span>
                        <span className="ts-tname">{teamName(sl.code)}</span>
                      </div>
                      <div className="ts-sec">
                        <div className="ts-sechead out"><span className="ts-arrow out">&#8593;</span><span className="ts-sectitle">Sends</span><span className="ts-seccount">{sends.length} asset{sends.length !== 1 ? "s" : ""}</span></div>
                        {sends.length === 0 && <div className="ts-noassets">&mdash;</div>}
                        {sends.map((x, j) => (
                          <div className="ts-sm" key={j}>
                            <Ini x={x} sm />
                            <span className="ts-sm-name">{x.name}</span>
                            {x.pos && <span className="ts-pos">{x.pos}</span>}
                            {x.route && <span className="ts-dest"><Dot code={x.route} />to {x.route}</span>}
                            <span className="ts-sm-amt">{x.kind === "pick" ? "" : x.amt}</span>
                          </div>
                        ))}
                      </div>
                      <div className="ts-sec">
                        <div className="ts-sechead in"><span className="ts-arrow in">&#8595;</span><span className="ts-sectitle">Gets</span><span className="ts-seccount">{gets.length} asset{gets.length !== 1 ? "s" : ""}</span></div>
                        {gets.length === 0 && <div className="ts-noassets">&mdash;</div>}
                        {gets.map((x, j) => (
                          <div className="ts-asset" key={j}>
                            <Ini x={x} />
                            <div className="ts-asset-main">
                              <div className="ts-asset-name">{x.name}</div>
                              <div className="ts-asset-meta">{x.pos && <span className="ts-pos">{x.pos}</span>}{x.route && <span className="ts-dest"><Dot code={x.route} />from {x.route}</span>}</div>
                            </div>
                            <div className={`ts-asset-amt ${x.kind === "pick" ? "pick" : ""}`}>{x.amt}</div>
                          </div>
                        ))}
                      </div>
                      <div className="ts-foot">
                        <span className="ts-foot-label">Net Salary</span>
                        <span className={`ts-foot-val ${net >= 0 ? "pos" : "neg"}`}>{net >= 0 ? "+" : "−"}{fmt$(Math.abs(net))} <span className="sub">{net >= 0 ? "added" : "shed"}</span></span>
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          </div>
        </div>
        <div className="modal-foot"><button className="btn" onClick={onClose}>Close</button></div>
      </div>
    </div>
  );
}

Object.assign(window, { Modal, SignFAModal, AddPlayerModal, TradeMachineModal, DraftProspectModal, SnapshotModal, TradeHistoryModal, TradeSnapshotModal });
