/* Round 10 #0a: contract years remaining, "2+1T Years" format.
   Counts guaranteed years; appends each option flavor as Np / Nt / Nng.
   Pulls from `p.seasons` (2026-27 onward) for under-contract / option players,
   or from the cap-editor decision when the player has been signed in-app. */
function contractYrsLabel(p, decision) {
  if (decision?.kind === "signed") {
    const yrs = decision.years || 1;
    if (yrs <= 1) return "1 Year";
    const opt = decision.option;   // "team" | "player" | null
    const tag = opt === "player" ? "P" : opt === "team" ? "T" : null;
    if (!tag) return `${yrs} Years`;
    return `${yrs - 1}+1${tag} Years`;
  }
  const future = (p.seasons || []).filter(s => s.season >= "2026-27");
  if (future.length === 0) return null;
  const g  = future.filter(s => s.type === "guaranteed" || !s.type).length;
  const po = future.filter(s => s.type === "player_option").length;
  const to = future.filter(s => s.type === "team_option").length;
  // A non-guaranteed year carrying a guarantee floor reads as PARTIAL (PG);
  // a flat non-guaranteed year stays NG.
  const pg = future.filter(s => s.type === "partially_guaranteed" || (s.type === "non_guaranteed" && s.guarantee && s.guarantee.amount > 0)).length;
  const ng = future.filter(s => s.type === "non_guaranteed" && !(s.guarantee && s.guarantee.amount > 0)).length;
  const parts = [];
  if (g  > 0) parts.push(String(g));
  if (po > 0) parts.push(`${po}P`);
  if (to > 0) parts.push(`${to}T`);
  if (pg > 0) parts.push(`${pg}PG`);
  if (ng > 0) parts.push(`${ng}NG`);
  const total = g + po + to + pg + ng;
  return parts.join("+") + " " + (total === 1 ? "Year" : "Years");
}

/* ============================================================
   Roster rows — FLEX-based layout (not a table).

   The layout naturally adapts:
   - Wide: [photo] [name/meta] [salary] [decisions]
           gap collapses around content; no wasted whitespace.
   - Narrow: salary stacks under name (in the .info column);
             decisions stay on the right.
   - Very narrow: decisions wrap below; player-cell stays full-width.

   Edit state is OWNED BY THE TABLE wrapper so:
   - while one row is editing, every OTHER row's decision controls dim
     and ignore clicks.
   - Hold / Renounce in the editing row cancel the edit AND apply that
     decision (2ways behaviour).
   ============================================================ */

function PlainTable({ players, state, dispatch, kind, layout = "choice", faMode }) {
  const [editingId, setEditingId] = useState(null);
  const [editKind, setEditKind] = useState(null);
  const [editSalary, setEditSalary] = useState("");
  const [editYears, setEditYears] = useState(2);
  const [editOption, setEditOption] = useState(null);   // final-year P/T option
  const [editRaisePct, setEditRaisePct] = useState(null); // single raise rate (fraction)
  const [editRaises, setEditRaises] = useState([]);       // per-year step fractions
  const [pyExpanded, setPyExpanded] = useState(false);    // per-year breakdown shown?
  const [faEditKind, setFaEditKind] = useState(null);     // pending decision for the apron-FA CapSheetRow editor
  const editingPlayer = players.find(p => p.name === editingId);
  // CBA caps for the player being re-signed (Bird → 5y/8%, else 4y; 8% for
  // Early-Bird too, 5% otherwise). Informational/labeling — see helpers.
  const maxYears = maxContractYears(editingPlayer?.birdRights);
  const raiseRate = contractRaiseRate(editingPlayer?.birdRights);

  function startEdit(p, kind) {
    setEditingId(p.name);
    setEditKind(kind);
    const decision = state.decisions[p.name];
    if (kind === "override") {
      const cur = decision?.salaryOverride
                ?? p.seasons?.find(s => s.season === "2026-27")?.salary
                ?? 0;
      setEditSalary((cur / 1_000_000).toFixed(2));
    } else {
      const prior = p.priorSeasonSalary || p.lastSalary || 0;
      const def = decision?.salary ?? Math.round(prior * 1.15);
      setEditSalary((def / 1_000_000).toFixed(1));
      setEditYears(decision?.years || 2);
      setEditOption(decision?.option || null);
      const mx = contractRaiseRate(p.birdRights);
      setEditRaisePct(decision?.raisePct ?? mx);
      setEditRaises(decision?.raises ?? Array(Math.max(0, (decision?.years || 2) - 1)).fill(mx));
      setPyExpanded(!!decision?.raises);   // a stored per-year deal opens expanded
      setFaEditKind("signed");             // apron FA editor opens on the Sign decision
    }
  }
  function cancelEdit() { setEditingId(null); setEditKind(null); }
  function saveEdit() {
    if (!editingPlayer) return;
    const n = parseSalaryFromMField(editSalary);
    if (n == null || n <= 0) return;
    if (editKind === "override") {
      // The override pencil corrects a mis-imported figure (not a signing), so it allows any
      // real salary — a star's later max-deal year can top $70M — but rejects an absurd typo.
      // No 2026-27 single-season salary approaches 60% of the cap (≈ $99M), so that's a safe
      // sanity ceiling that still blocks e.g. a $300M fat-finger. (NOT the CBA signing max.)
      if (n > CAP_2026.cap * 0.6) {
        dispatch({ type: "SET_TOAST", toast: `That salary (${fmt$(n)}) looks too high to be a correction — please re-check the figure.` });
        return;
      }
      const prev = state.decisions[editingPlayer.name] || { kind: "keep" };
      dispatch({ type: "SET_DECISION", player: editingPlayer.name, decision: { ...prev, salaryOverride: n } });
    } else {
      const yrs = state.multiYear ? editYears : 1;
      const decision = { kind: "signed", salary: n, years: yrs, option: yrs > 1 ? editOption : null };
      if (state.adjustRaises && yrs > 1) {
        if (pyExpanded && yrs > 2) {
          const mx = contractRaiseRate(editingPlayer.birdRights);
          decision.raises = Array.from({ length: yrs - 1 }, (_, i) => editRaises[i] ?? mx);
        } else decision.raisePct = editRaisePct;
      }
      if (reSignBlocked(editingPlayer, { salary: decision.salary, years: decision.years || 1, raisePct: decision.raisePct, raises: decision.raises }, dispatch, state)) return;
      dispatch({ type: "SET_DECISION", player: editingPlayer.name, decision });
    }
    cancelEdit();
  }
  function setDec(p, kind, extra = {}) {
    if (editingId === p.name) cancelEdit();
    dispatch({ type: "SET_DECISION", player: p.name, decision: { kind, ...extra } });
  }

  if (players.length === 0) return null;
  const isStatus = layout === "status";
  const has3btn = !isStatus && players.some(p => p.offseasonStatus === "UFA" || p.offseasonStatus === "RFA");

  return (
    <section className={`section flex-tbl ${isStatus ? "status-tbl" : "decision-tbl"} ${has3btn ? (faMode === "apron" ? "one-btn" : "three-btn") : "two-btn"} ${editingId ? "has-edit" : ""}`}>
      <div className="flex-row header">
        <div className="hdr-photo-spacer" aria-hidden="true" />
        <div className="info-col">
          <span className="hdr-wide">PLAYER</span>
          <span className="hdr-narrow">Salary</span>
        </div>
        <div className="salary-col">Salary</div>
        <div className="row-spacer" />
        <div className="decision-col"><span className="hdr-dec">{isStatus ? "Status" : "Decision"}</span></div>
      </div>
      {players.map(p => (
        // Apron free agent being re-signed → use the SAME CapSheetRow editor as
        // the Roster (decision buttons + contextual salary box + advice). The
        // idle rows (and lite teams, and cap mode) stay on RosterRow.
        (faMode === "apron" && editingId === p.name && editKind === "re-sign" && !p._lite)
          ? <CapSheetRow key={p.name} p={p._optStatus ? { ...p, offseasonStatus: p._optStatus } : p}
                         state={state} dispatch={dispatch}
                         salary={computeCapHold(p)} editing apronView onEdit={() => {}}
                         editVal={editSalary} setEditVal={setEditSalary}
                         editKind={faEditKind} setEditKind={setFaEditKind}
                         dirty={false} anyEditing={false} onClose={cancelEdit}
                         editYears={editYears} setEditYears={setEditYears}
                         editOption={editOption} setEditOption={setEditOption}
                         editRaisePct={editRaisePct} setEditRaisePct={setEditRaisePct}
                         editRaises={editRaises} setEditRaises={setEditRaises}
                         pyExpanded={pyExpanded} setPyExpanded={setPyExpanded} />
          : <RosterRow key={p.name} player={p} state={state} layout={layout} faMode={faMode}
                   isEditing={editingId === p.name}
                   hasOtherEditing={editingId != null && editingId !== p.name}
                   editKind={editKind} editSalary={editSalary} editYears={editYears}
                   setEditSalary={setEditSalary} setEditYears={setEditYears}
                   editOption={editOption} setEditOption={setEditOption}
                   editRaisePct={editRaisePct} setEditRaisePct={setEditRaisePct}
                   editRaises={editRaises} setEditRaises={setEditRaises}
                   pyExpanded={pyExpanded} setPyExpanded={setPyExpanded}
                   maxYears={maxYears} raiseRate={raiseRate}
                   startEdit={(kind) => startEdit(p, kind)}
                   cancelEdit={cancelEdit} saveEdit={saveEdit}
                   setDec={(kind, extra) => setDec(p, kind, extra)}
                   dispatch={dispatch} />
      ))}
    </section>
  );
}

function RosterRow({ player: p, state, layout = "choice", faMode,
                    isEditing, hasOtherEditing,
                    editKind, editSalary, editYears,
                    setEditSalary, setEditYears,
                    editOption, setEditOption, maxYears, raiseRate,
                    editRaisePct, setEditRaisePct, editRaises, setEditRaises,
                    pyExpanded, setPyExpanded,
                    startEdit, cancelEdit, saveEdit,
                    setDec, dispatch }) {
  const decision = state.decisions[p.name];
  // Effective FA mode: cap step-3 ("Final Roster") passes faMode="apron"
  // so the actions/format read as apron even though state.mode is "cap".
  const fmMode = faMode || state.mode;
  const effSalary = playerEffectiveSalary(p, state.decisions, state.mode);
  const photoSrc = p.nbaId ? `assets/players/${p.nbaId}.png` : null;

  const dimRow = decision?.kind === "waive" || decision?.kind === "decline" || decision?.kind === "renounced" || decision?.kind === "traded";
  const isUFAOrRFA = p.offseasonStatus === "UFA" || p.offseasonStatus === "RFA";
  const showHoldFormula = fmMode === "cap" && isUFAOrRFA
                        && (decision?.kind === "kept-hold" || !decision);
  const signed = decision?.kind === "signed";

  // Non-blocking Bird-rights hint while re-signing in the Cap stage.
  const editSalNum = isEditing ? parseSalaryFromMField(editSalary) : null;
  const reSignNudge = (isEditing && editKind === "re-sign" && fmMode === "cap"
                       && birdResignNudge(p, editSalNum))
    ? `${p.name} has ${p.birdRights === "Bird" ? "Bird" : "Early Bird"} rights — re-signing above the cap hold is better done in the Apron stage (it won't use cap room).`
    : null;

  function restore() {
    dispatch({ type: "SET_DECISION", player: p.name, decision: { kind: "kept-hold" } });
  }
  // (B) The per-player "↺ Undo trade" now opens the trade-undo window focused on THIS
  // player's trade (button below) by dispatching a window event the App listens for.
  // It replaces the old per-player untrade(), which restored just this player's
  // decision and left the counterpart's incoming half — breaking trade matching.

  // Apron re-sign editor — mirrors the Cap Moves editor (CapSheetRow): photo +
  // controls on top, then a full-width bar with the total + big bordered ✓/✗.
  // Replaces the whole row while editing, so there's no separate Re-sign button.
  if (isEditing && editKind === "re-sign") {
    const base = parseSalaryFromMField(editSalary) || 0;
    const BR = { Bird: "Bird Rights", EarlyBird: "Early Bird", NonBird: "Non-Bird" };
    const birdLabel = BR[p.birdRights] || "Free agent";
    // Gear: multiYear gates the years picker; adjustRaises shows the raise
    // stepper (off = max raises). Per-year is the in-editor expand.
    const multiYear = state.multiYear;
    const adjustRaises = state.adjustRaises;
    const yrs = multiYear ? editYears : 1;
    const maxRate = raiseRate;
    const calc = contractCalc(base, yrs, { adjustRaises, raisePct: editRaisePct, raises: editRaises, pyExpanded, maxRate });
    // No Option row (1-year) → ✓/✗ go inline beside the salary box and the
    // reference rides the name line (saves a row); multi-year keeps the ref in
    // the ref-row + the bottom Option + ✓/✗ bar.
    const inlineConfirm = !multiYear;
    const refEl = <span className="cap-locked"><span className="cap-lbl">{birdLabel}</span></span>;
    return (
      <div className="flex-row body-row cap-row editing resign-edit">
          {photoSrc
            ? <img className="row-photo" src={photoSrc} alt="" onError={(e) => { e.currentTarget.style.display = "none"; e.currentTarget.nextSibling.style.display = "grid"; }} />
            : null}
          <span className="row-photo placeholder" style={{ display: photoSrc ? "none" : "grid" }}>{p.name.split(" ").map(s => s[0]).slice(0, 2).join("")}</span>
          <div className="info-col">
            <div className="cap-line1">
              <div className="player-name">{p.name}{p.position && <span className="pos-name">{p.position}</span>}</div>
              {refEl}
            </div>
            <div className="cap-ref-row">
              <span className="cap-edit-line">
                <span className="cap-lbl salary-hdr">Salary</span>
                <span className="salary-edit inline-final">
                  <span className="pre">$</span>
                  <input autoFocus type="text" inputMode="decimal" value={editSalary}
                         onChange={(e) => setEditSalary(e.target.value)} onFocus={(e) => e.target.select()}
                         onKeyDown={(e) => { if (e.key === "Enter") saveEdit(); if (e.key === "Escape") cancelEdit(); }} />
                  {mFieldShowM(editSalary) && <span className="pre" style={{ fontSize: 12 }}>M</span>}
                </span>
                {inlineConfirm && <ConfirmButtons onSave={saveEdit} onCancel={cancelEdit} saveTitle="Re-sign" />}
              </span>
            </div>
            {multiYear && (
              <ContractFields base={base} years={editYears} setYears={setEditYears}
                              maxYears={maxYears} maxRate={maxRate} calc={calc}
                              setRaisePct={setEditRaisePct} setRaises={setEditRaises}
                              setPyExpanded={setPyExpanded} />
            )}
          </div>
        {multiYear && calc.usePerYear && base > 0 && yrs > 1 && (
          <ContractPerYear calc={calc} maxRate={maxRate} setRaises={setEditRaises} />
        )}
        {!inlineConfirm && (
          <OptionConfirmBar option={editOption} setOption={setEditOption} years={editYears}>
            <ConfirmButtons onSave={saveEdit} onCancel={cancelEdit} saveTitle="Re-sign" />
          </OptionConfirmBar>
        )}
      </div>
    );
  }

  return (
    <>
    <div className={`flex-row body-row ${isEditing ? "editing" : ""} ${dimRow ? "decided-out" : ""} ${hasOtherEditing ? "other-editing" : ""}`}>
      {photoSrc
        ? <img className="row-photo" src={photoSrc} alt="" onError={(e) => { e.currentTarget.style.display = "none"; e.currentTarget.nextSibling.style.display = "grid"; }} />
        : null}
      <div className="row-photo placeholder" style={{ display: photoSrc ? "none" : "grid" }}>
        {p.name.split(" ").map(s => s[0]).slice(0, 2).join("")}
      </div>

      <div className="info-col">
        <div className="player-name">{p.name}
          {p.position && <span className="pos-name">{p.position}</span>}</div>
        <div className="player-sub">
          {p.position && <span className="pos-badge pos-meta">{p.position}</span>}
          {p.offseasonStatus === "RFA" && <span className="status-chip rfa">RFA</span>}
          {(() => {
            const bits = [];
            if (p.birdRights && p.offseasonStatus !== "under_contract") {
              const BR = { Bird: "Bird Rights", EarlyBird: "Early Bird", NonBird: "Non-Bird" };
              bits.push({ label: BR[p.birdRights] || `${p.birdRights} rights`, cls: "bird-meta" });
            }
            if (p.contractType === "max") bits.push({ label: "max contract", cls: "max-meta" });
            return bits.map((b, i) => (
              <span key={i} className={b.cls}>{i > 0 && <span style={{ color: "var(--text-faint)", margin: "0 6px" }}>·</span>}{b.label}</span>
            ));
          })()}
          {dimRow && decision?.kind === "renounced" && (
            <button className="restore-link" onClick={restore} title="Bring back as cap hold">↺ Restore</button>
          )}
          {decision?.kind === "traded" && (
            <button className="restore-link"
                    onClick={() => window.dispatchEvent(new CustomEvent("open-trade-history", { detail: { tradeId: decision?.tradeId } }))}
                    title="Show this player's trade — undo it (for all teams) from the trade list">↺ Undo trade</button>
          )}
        </div>
        {/* salary moves INTO info-col at narrow widths via CSS */}
        <div className="salary-inline">
          {isEditing
            ? <SalaryEditField value={editSalary} onChange={setEditSalary}
                               onSave={saveEdit} onCancel={cancelEdit}
                               compact />
            : <SalaryDisplay p={p} decision={decision} effSalary={effSalary}
                             dimRow={dimRow} signed={signed} state={state}
                             showHoldFormula={showHoldFormula}
                             startEdit={startEdit} />}
        </div>
      </div>

      <div className="salary-col">
        {isEditing ? (
          <SalaryEditField value={editSalary} onChange={setEditSalary}
                           onSave={saveEdit} onCancel={cancelEdit} />
        ) : (
          <SalaryDisplay p={p} decision={decision} effSalary={effSalary}
                         dimRow={dimRow} signed={signed} state={state}
                         showHoldFormula={showHoldFormula}
                         startEdit={startEdit} />
        )}
      </div>

      <div className="row-spacer" />

      <div className="decision-col">
        {/* Partial-guarantee cost, on the RIGHT by the Keep/Waive control (it IS the
            cost of waiving). Kept out of the salary cell so salaries align across rows. */}
        {(p.offseasonStatus === "non_guaranteed" || p.offseasonStatus === "partially_guaranteed") && isPartial(p) && layout !== "status" && decision?.kind !== "traded" && (
          <span className="pg-hint">
            {decision?.kind === "waive" ? "dead money if waived" : `${fmt$(guaranteedAmountFor(p))} gtd if waived`}
          </span>
        )}
        {decision?.kind === "traded"
          ? <span className="badge renounced"><span className="dot" />Traded away</span>
          : layout === "status"
          ? <span className="badge guar"><span className="dot" />Guaranteed</span>
          : <RowActions p={p} decision={decision} isEditing={isEditing}
                        hasOtherEditing={hasOtherEditing} mode={fmMode}
                        setDec={setDec}
                        startResign={() => startEdit("re-sign")} />}
      </div>
    </div>
    {reSignNudge && (
      <div className="fa-alert-row">
        <div className="alert alert-info" role="status">
          <span className="alert-ico">ⓘ</span>
          <div className="alert-body">{reSignNudge}</div>
        </div>
      </div>
    )}
    </>
  );
}

function SalaryDisplay({ p, decision, effSalary, dimRow, signed, state, showHoldFormula, startEdit }) {
  return (
    <>
      <span className="money">
        {effSalary > 0 ? (
          <>
            <Money n={effSalary} dim={dimRow} />
            {signed && (
              <button className="pencil tiny" onClick={() => startEdit("re-sign")} title="Edit signing">
                <Icon.Pencil />
              </button>
            )}
            {(p.offseasonStatus === "non_guaranteed" || p.offseasonStatus === "partially_guaranteed") && !dimRow && (
              <button className="pencil tiny" onClick={() => startEdit("override")} title="Edit salary (in case the source value is wrong)">
                <Icon.Pencil />
              </button>
            )}
          </>
        ) : (
          <span className="dim">$0</span>
        )}
      </span>
      {(() => {
        // #0a: years remaining on contract, "2+1T Years" — replaces the old
        // signed-only "Ny · TO deal" tag and also shows for under-contract /
        // option players. Hidden for fully-departed (traded/renounced/waived).
        if (decision?.kind === "traded" || decision?.kind === "renounced" || decision?.kind === "waive") return null;
        const label = contractYrsLabel(p, decision);
        if (!label) return null;
        return <span className="contract-yrs">{label}{decision?.kind === "signed" ? " deal" : ""}</span>;
      })()}
      {decision?.salaryOverride != null && (p.offseasonStatus === "non_guaranteed" || p.offseasonStatus === "partially_guaranteed") && (
        <span className="override-tag">manual override</span>
      )}
      {/* Partial-guarantee hint moved OUT of the salary cell → into the decision-col
          (right side, by Keep/Waive) so it no longer clutters/misaligns the salary. */}
      {showHoldFormula && (
        <span className="formula-inline">{holdFormula(p)}</span>
      )}
    </>
  );
}

function Money({ n, dim }) {
  return (
    <>
      <span className="money-full" style={dim ? { color: "var(--text-faint)" } : undefined}>{fmt$Full(n)}</span>
      <span className="money-compact" style={dim ? { color: "var(--text-faint)" } : undefined}>{fmt$(n)}</span>
    </>
  );
}

/* Compact raise stepper (− [+8%] +). value/max are fractions; steps 1%,
   clamped to [-max, +max]. Used by the re-sign editor and the Sign-FA modal. */
function RaiseStepper({ value, max, onChange }) {
  const pct = Math.round((value || 0) * 100);
  const maxPct = Math.round(max * 100);
  const set = (p) => onChange(Math.max(-maxPct, Math.min(maxPct, p)) / 100);
  return (
    <span className="raise-stepper">
      <button type="button" onClick={() => set(pct - 1)} disabled={pct <= -maxPct} title="Lower">−</button>
      <span className="raise-val">{pct > 0 ? "+" : ""}{pct}%</span>
      <button type="button" onClick={() => set(pct + 1)} disabled={pct >= maxPct} title="Raise">+</button>
    </span>
  );
}

/* Shared contract-raise math + fields, reused by the apron re-sign editor,
   the edited-addition editor, and the cap "Cap Moves" editor. */
function contractCalc(base, years, { adjustRaises, raisePct, raises, pyExpanded, maxRate }) {
  const effRate = raisePct == null ? maxRate : raisePct;
  // Per-year only matters for 3+ years (a 2-yr deal has a single raise = the
  // single percentage), so it collapses to the single stepper below that.
  const usePerYear = adjustRaises && pyExpanded && years > 2;
  const showSingle = adjustRaises && !usePerYear;
  const raisesArr = Array.from({ length: Math.max(0, years - 1) },
    (_, i) => (raises && raises[i] != null ? raises[i] : maxRate));
  const opts = usePerYear ? { raises: raisesArr } : showSingle ? { rate: effRate } : { rate: maxRate };
  return { effRate, usePerYear, showSingle, raisesArr,
           yearSal: contractYearSalaries(base, years, opts),
           total: contractTotalFrom(base, years, opts) };
}
/* Years segmented field + Total line (single raise stepper + "Per year ▾"
   expand). The per-year list itself is <ContractPerYear> (separate grid row). */
function ContractFields({ base, years, setYears, maxYears, maxRate, calc, setRaisePct, setRaises, setPyExpanded, hideYearsHeader }) {
  const { effRate, usePerYear, showSingle, total } = calc;
  const yearList = Array.from({ length: maxYears || 4 }, (_, i) => i + 1);
  const expandPY = () => { setRaises(Array.from({ length: Math.max(0, years - 1) }, () => effRate)); setPyExpanded(true); };
  return (
    <>
      <div className="resign-field">
        {!hideYearsHeader && <span className="resign-hdr">Years</span>}
        <div className="decisions equal resign-years">
          {yearList.map(y => (
            <button key={y} className={years === y ? "on-pick" : ""} onClick={() => setYears(y)}>{y}</button>
          ))}
        </div>
      </div>
      {base > 0 && years > 1 ? (
        <div className="contract-total">
          {showSingle ? (
            <>Total ≈ <span className="ct-amt">{fmt$(total)}</span> over {years} yrs · raises <RaiseStepper value={effRate} max={maxRate} onChange={setRaisePct} />
              {years > 2 && <button type="button" className="py-toggle" onClick={expandPY}>Per year ▾</button>}
            </>
          ) : usePerYear ? (
            <>Total ≈ {fmt$(total)} over {years} yrs · per-year <button type="button" className="py-toggle" onClick={() => setPyExpanded(false)}>▴</button></>
          ) : (
            <>Total ≈ up to {fmt$(total)} over {years} yrs · max {Math.round(maxRate * 100)}% raises</>
          )}
        </div>
      ) : (
        <div className="contract-total">Enter the first-year salary.</div>
      )}
    </>
  );
}
function ContractPerYear({ calc, maxRate, setRaises }) {
  const { raisesArr, yearSal } = calc;
  const setRaiseAt = (i, v) => { const a = raisesArr.slice(); a[i] = v; setRaises(a); };
  return (
    <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={maxRate} onChange={(v) => setRaiseAt(i, v)} />
          <span className="raise-py-amt">{fmt$(yearSal[i + 1])}</span>
        </div>
      ))}
    </div>
  );
}
/* The bottom bar shared by every signing editor: the OPTION label (photo
   column) + None/Player/Team control (info column, when showOption), then the
   ✓/✗ (+ optional trash) the caller passes as children. */
function OptionConfirmBar({ option, setOption, years, children }) {
  // A 1-year deal can't carry a final-year option — lock to None + grey out.
  const lockNone = years != null && years <= 1;
  const cur = lockNone ? "none" : (option || "none");
  return (
    <div className="cap-advice-bar resign-bar">
      <span className="resign-hdr">Option</span>
      <div className="resign-bar-main">
        <div className="decisions equal three resign-opt">
          {[["none", "None"], ["player", "Player"], ["team", "Team"]].map(([v, lbl]) => (
            <button key={v} className={cur === v ? "on-pick" : ""} disabled={lockNone}
                    onClick={() => { if (!lockNone) setOption(v === "none" ? null : v); }}>{lbl}</button>
          ))}
        </div>
        <div className="cap-edit-confirm">{children}</div>
      </div>
    </div>
  );
}
/* The ✓/✗ (+ optional trash) cluster, shared so it can render either inline
   beside the salary or inside the bottom OptionConfirmBar (gear toggle). */
function ConfirmButtons({ onSave, onCancel, onRemove, saveTitle = "Save", removeTitle = "Remove" }) {
  return (
    <div className="cap-edit-confirm">
      {onRemove && <button className="pencil cancel" title={removeTitle} onClick={onRemove}><Icon.Trash /></button>}
      <button className="pencil confirm" title={saveTitle} onClick={onSave}><Icon.Check /></button>
      <button className="pencil cancel" title="Cancel" onClick={onCancel}><Icon.X /></button>
    </div>
  );
}

/* Salary-only inline editor (committed/override salary edits). The apron
   re-sign editor with years/option/total is the dedicated block in RosterRow. */
function SalaryEditField({ value, onChange, onSave, onCancel, compact }) {
  const ref = useRef();
  useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
  return (
    <div className={`salary-edit-wrap ${compact ? "compact" : ""}`}>
      <div className="salary-edit">
        <span className="pre">$</span>
        <input ref={ref} type="text" inputMode="decimal" value={value} onChange={(e) => onChange(e.target.value)}
               onKeyDown={(e) => { if (e.key === "Enter") onSave(); if (e.key === "Escape") onCancel(); }} />
        <span className="pre" style={{ fontSize: 12 }}>M</span>
      </div>
      <div className="salary-edit-foot">
        <span style={{ flex: 1 }} />
        <button className="pencil cancel" onClick={onCancel} title="Cancel"><Icon.X /></button>
        <button className="pencil confirm" onClick={onSave} title="Confirm"><Icon.Check /></button>
      </div>
    </div>
  );
}

function RowActions({ p, decision, isEditing, hasOtherEditing, mode, setDec, startResign }) {
  const locked = hasOtherEditing;

  switch (p.offseasonStatus) {
    case "under_contract":
      return <span className="badge guar"><span className="dot" />Guaranteed</span>;
    case "partially_guaranteed":   // FIX: same Keep / Waive control as a non-guaranteed row
    case "non_guaranteed":
      return (
        <Toggle locked={locked}
          left={{ key: "keep",  label: "Keep",  on: decision?.kind === "keep" }}
          right={{ key: "waive", label: "Waive", on: decision?.kind === "waive" }}
          onChange={(k) => setDec(k)} />
      );
    case "player_option": {
      // Opting out is the gateway to becoming a FA — keep it shown as
      // selected even after the player has been re-signed / held /
      // renounced downstream (those states all imply they opted out).
      const optedOut = ["opt-out", "signed", "kept-hold", "renounced"].includes(decision?.kind);
      return (
        <Toggle locked={locked}
          left={{ key: "opt-in",  label: "Opts in",  on: decision?.kind === "opt-in" }}
          right={{ key: "opt-out", label: "Opts out", on: optedOut, pol: "neutral" }}
          onChange={(k) => setDec(k)} />
      );
    }
    case "team_option": {
      const declined = ["decline", "signed", "kept-hold", "renounced"].includes(decision?.kind);
      return (
        <Toggle locked={locked}
          left={{ key: "exercise", label: "Exercise", on: decision?.kind === "exercise" }}
          right={{ key: "decline",  label: "Decline", on: declined, pol: "neutral" }}
          onChange={(k) => setDec(k)} />
      );
    }
    case "UFA":
    case "RFA":
      return <FAActions decision={decision} isEditing={isEditing} locked={locked}
                        mode={mode} setDec={setDec} startResign={startResign} />;
    default: return null;
  }
}

function Toggle({ left, right, onChange, locked }) {
  const leftOn = left.on;
  const rightOn = right.on;
  // colour follows MEANING (pol): pos→green ✓ · neg→red ✗ · neutral→grey · pick→blue.
  const cls = { pos: "on-yes", neg: "on-no", neutral: "on-neutral", pick: "on-pick" };
  return (
    <div className={`decisions equal two ${locked ? "locked" : ""}`}>
      <button className={leftOn ? (cls[left.pol] || "on-yes") : ""} disabled={locked}
              onClick={() => !leftOn && !locked && onChange(left.key)}>
        {leftOn && <Icon.Check className="dec-icon" />}
        <span className="dec-label">{left.label}</span>
      </button>
      <button className={rightOn ? (cls[right.pol] || "on-no") : ""} disabled={locked}
              onClick={() => !rightOn && !locked && onChange(right.key)}>
        {rightOn && right.pol !== "neutral" && <Icon.X className="dec-icon" />}
        <span className="dec-label">{right.label}</span>
      </button>
    </div>
  );
}

/* Chunk 2 decision control: nothing pre-selected; a Check on the CHOSEN button,
   the other stays neutral (NO X). `value` may be null (no choice made yet). */
function DecisionPick({ options, value, onChange, defaultPol = "pos" }) {
  // colour follows MEANING (pol per option): pos→green ✓ · neg→red ✗ ·
  // neutral→grey (pending — the NEXT choice sets good/bad, no icon) · pick→blue.
  const cls = { pos: "on-yes", neg: "on-no", neutral: "on-neutral", pick: "on-pick" };
  return (
    <div className="decisions equal two decide-pick">
      {options.map(o => {
        const on = value === o.key;
        const pol = o.pol || defaultPol;
        return (
          <button key={o.key} className={on ? cls[pol] : ""} aria-pressed={on}
                  onClick={() => onChange(o.key)}>
            {on && pol === "pos" && <Icon.Check className="dec-icon" />}
            {on && pol === "neg" && <Icon.X className="dec-icon" />}
            <span className="dec-label">{o.label}</span>
          </button>
        );
      })}
    </div>
  );
}

function FAActions({ decision, isEditing, locked, mode, setDec, startResign }) {
  const signed = decision?.kind === "signed";
  const hold = decision?.kind === "kept-hold" || !decision;
  const renounced = decision?.kind === "renounced";
  const reSignActive = signed || isEditing;

  // Apron mode: cap holds don't count, so Hold is the implicit default
  // and Renounce is meaningless — collapse to a single Re-sign control.
  if (mode === "apron") {
    return (
      <div className={`decisions resign-only ${isEditing ? "during-edit" : ""} ${locked ? "locked" : ""}`}>
        <button className={reSignActive ? "on-yes" : ""} disabled={locked}
                onClick={() => { if (locked || isEditing) return; if (!signed) startResign(); }}>
          {reSignActive && <Icon.Check className="dec-icon" />}
          <span className="dec-label">{signed ? "Re-signed" : "Re-sign"}</span>
        </button>
      </div>
    );
  }

  return (
    <div className={`decisions equal three ${isEditing ? "during-edit" : ""} ${locked ? "locked" : ""}`}>
      <button className={reSignActive ? "on-yes" : ""} disabled={locked}
              onClick={() => { if (locked || isEditing) return; if (!signed) startResign(); }}>
        {reSignActive && <Icon.Check className="dec-icon" />}
        <span className="dec-label">Re-sign</span>
      </button>
      <button className={hold && !isEditing ? "on-neutral" : ""} disabled={locked}
              onClick={() => !locked && setDec("kept-hold")}>
        <span className="dec-label">Hold</span>
      </button>
      <button className={renounced && !isEditing ? "on-no" : ""} disabled={locked}
              onClick={() => !locked && setDec("renounced")}>
        <span className="dec-label">Renounce</span>
      </button>
    </div>
  );
}

Object.assign(window, { PlainTable, RosterRow, Toggle, DecisionPick, Money, RaiseStepper, contractCalc, ContractFields, ContractPerYear, OptionConfirmBar, ConfirmButtons, contractYrsLabel });
