/* [FA-REDESIGN PROTOTYPE — pass 6] The "Free agents" hub, integrated full-width.
   This round (owner notes a–k): single row layout (A-style logo cell at 60px),
   photos with NO background (match roster), stretched/taller logo with the divider
   moved BETWEEN logo and Sign (+ more padding on wide screens), search goes partway
   with the team toggle on its right, search uses var(--bg-input) not the naked bg,
   Rest-of-League rows drop the Bird tag (always non-Bird), the ladder lists only
   AVAILABLE exceptions, default salary = previous year's. Editor polish (g) pinned. */
(function () {
  if (typeof window !== "undefined") window.__FA_PROTO_ACTIVE = true;
  const { useState } = React;

  const C = (typeof CAP_2026 !== "undefined" && CAP_2026) || {};
  const apron1 = (C.apron1 || 209.1e6) / 1e6, apron2 = (C.apron2 || 221.7e6) / 1e6;
  const m = n => "$" + (Math.round(n * 10) / 10).toFixed(1) + "M";
  const ini = s => (s || "").split(" ").map(w => w[0]).join("").slice(0, 2).toUpperCase();
  const BASE_MOCK = 196.0;   // fallback only — the real per-team BASE is computed inside OfferSheet from the capBase prop (J1a)
  const TOOLS = { cap: { n: "Cap space", max: 1e9, y: 4, hard: null }, ntmle: { n: "Full MLE", max: (C.mle ? C.mle / 1e6 : 15.0), y: 4, hard: "apron1" }, tpmle: { n: "Tax MLE", max: (C.tpmle ? C.tpmle / 1e6 : 5.7), y: 2, hard: "apron2" }, bae: { n: "BAE", max: (C.bae ? C.bae / 1e6 : 5.4), y: 2, hard: "apron1" }, room: { n: "Room MLE", max: (C.roomMle ? C.roomMle / 1e6 : 9.4), y: 3, hard: null }, min: { n: "Vet Min", max: 3.0, y: 2, hard: null } };
  const ORDER = ["cap", "ntmle", "tpmle", "bae", "room", "min"];
  const METHODS_BY_TIER = { nonTax: ["ntmle", "bae", "min"], tax: ["tpmle", "min"], apron2plus: ["min"] };
  // method id → the engine `source` string the roster/exception logic keys off (must match the native Sign-FA modal).
  const SOURCE_BY_METHOD = { ntmle: "MLE", tpmle: "TPMLE", bae: "BAE", cap: "Cap space", room: "Room MLE", min: "Vet min" };
  const KEY_BY_SOURCE = { "Cap space": "cap", "MLE": "ntmle", "TPMLE": "tpmle", "BAE": "bae", "Room MLE": "room", "Vet min": "min" };
  const HARDCAP_AT = { "MLE": "firstApron", "BAE": "firstApron", "TPMLE": "secondApron" };
  const birdOf = b => { const s = ("" + (b || "")).toLowerCase(); return s.includes("early") ? "Early-Bird" : s.includes("non") ? "Non-Bird" : s.includes("bird") ? "Bird" : "none"; };
  // [§4.3] a player's REAL YOS-scaled veteran minimum, in $M (from the deployed dataset's
  // minScale — the same table signingLegality bounds a "Vet min" signing to). Replaces the
  // flat $3.0M mock so the Vet-Min tool shows/defaults to the right figure per player.
  const minForYosRaw = yos => { const ms = (C && C.minScale) || (typeof window !== "undefined" && window.__tmData && window.__tmData.meta && window.__tmData.meta.minScale) || null; const v = (ms && yos != null) ? Number(ms[Math.min(Math.max(yos, 0), 10)]) : null; return Number.isFinite(v) ? v : null; };
  const minForYosM = yos => { const r = minForYosRaw(yos); return r != null ? r / 1e6 : null; };
  // FA hub now surfaces option players too (TO = team option, PO = player option) alongside UFA/RFA.
  const FA_STATUSES = { UFA: 1, RFA: 1, team_option: 1, player_option: 1 };
  const STLABEL = { UFA: "UFA", RFA: "RFA", team_option: "TO", player_option: "PO" };
  const ST_OPTS = [["UFA", "UFA"], ["RFA", "RFA"], ["team_option", "Team Option"], ["player_option", "Player Option"]];
  const ALL_ST = ST_OPTS.map(o => o[0]);
  const LOGO_BY_CODE = { ATL: "atlanta_hawks_1610612737", BOS: "boston_celtics_1610612738", BKN: "brooklyn_nets_1610612751", CHA: "charlotte_hornets_1610612766", CHI: "chicago_bulls_1610612741", CLE: "cleveland_cavaliers_1610612739", DAL: "dallas_mavericks_1610612742", DEN: "denver_nuggets_1610612743", DET: "detroit_pistons_1610612765", GSW: "golden_state_warriors_1610612744", HOU: "houston_rockets_1610612745", IND: "indiana_pacers_1610612754", LAC: "la_clippers_1610612746", LAL: "los_angeles_lakers_1610612747", MEM: "memphis_grizzlies_1610612763", MIA: "miami_heat_1610612748", MIL: "milwaukee_bucks_1610612749", MIN: "minnesota_timberwolves_1610612750", NOP: "new_orleans_pelicans_1610612740", NYK: "new_york_knicks_1610612752", OKC: "oklahoma_city_thunder_1610612760", ORL: "orlando_magic_1610612753", PHI: "philadelphia_76ers_1610612755", PHX: "phoenix_suns_1610612756", POR: "portland_trail_blazers_1610612757", SAC: "sacramento_kings_1610612758", SAS: "san_antonio_spurs_1610612759", TOR: "toronto_raptors_1610612761", UTA: "utah_jazz_1610612762", WAS: "washington_wizards_1610612764" };

  function Logo({ code }) { const f = LOGO_BY_CODE[code]; if (!f) return null; return <img className="fahub-logo-img" src={`assets/logos/${f}.svg`} alt={code} onError={e => { e.currentTarget.style.visibility = "hidden"; }} />; }
  function Photo({ nbaId, name }) {
    const [stage, setStage] = useState(0);
    if (!nbaId || stage >= 2) return <span className="fahub-photo placeholder">{ini(name)}</span>;
    const src = stage === 0 ? `assets/players/${nbaId}.png` : `https://cdn.nba.com/headshots/nba/latest/260x190/${nbaId}.png`;
    return <img className="fahub-photo" loading="lazy" src={src} alt="" onError={() => setStage(s => s + 1)} />;
  }
  function EditorPhoto({ nbaId, name }) {
    const [stage, setStage] = useState(0);
    const showImg = nbaId && stage < 2;
    const src = showImg ? (stage === 0 ? `assets/players/${nbaId}.png` : `https://cdn.nba.com/headshots/nba/latest/260x190/${nbaId}.png`) : "";
    return (
      <>
        {showImg && <img className="row-photo fahub-rowphoto" loading="lazy" src={src} alt="" onError={() => setStage(s => s + 1)} />}
        <span className="row-photo placeholder" style={{ display: showImg ? "none" : "grid" }}>{ini(name)}</span>
      </>
    );
  }
  const teamName2 = code => { const f = LOGO_BY_CODE[code]; return f ? f.split("_").slice(0, -1).map(w => (w === "la" ? "LA" : w[0].toUpperCase() + w.slice(1))).join(" ") : code; };
  function buildFAs(allTeams, teamCode, nbaIdByName) {
    const teams = allTeams || (typeof window !== "undefined" && window.__tmData && window.__tmData.teams) || {};
    const out = [];
    for (const code of Object.keys(teams)) for (const p of ((teams[code] && teams[code].players) || [])) {
      if (FA_STATUSES[p.offseasonStatus]) {
        // TO/PO players are a 2026 decision only if 2026-27 is the last contract season.
        const isOpt = p.offseasonStatus === "team_option" || p.offseasonStatus === "player_option";
        const optNotTerminal = isOpt && (p.seasons || []).some(s => parseInt(s.season, 10) >= 2027);
        if (optNotTerminal) continue;
        const nbaId = p.nbaId || (nbaIdByName && nbaIdByName[p.name]) || null;
        out.push({ id: code + ":" + p.name, nm: p.name, pos: p.position || "SF", team: code, own: code === teamCode, status: p.offseasonStatus, nbaId, bird: birdOf(p.birdRights), prior: (p.priorSeasonSalary || 0) / 1e6, hold: (p.capHold || 0) / 1e6, birdRights: p.birdRights, yos: p.yearsOfExperience, designated: !!p.designated });
      }
    }
    out.sort((a, b) => {
      const rank = (x) => (x.status === "UFA" || x.status === "RFA" ? 0 : 1);
      return rank(a) - rank(b) || b.prior - a.prior || a.nm.localeCompare(b.nm);
    });
    return out;
  }

  // One-world index across ALL teams in the active scenario. Two layers:
  //  • signedTo[name] = { team, mode, kind:"add"|"resign", id? } — a player the USER
  //    signed/re-signed somewhere → greyed "Signed to X" + undo (unchanged behavior).
  //  • optBy["team:name"] = { team, status:"staying"|"available", kind } — the owning team's
  //    OPTION decision. opt-in/exercise → "staying" (under his option, not signable
  //    elsewhere). opt-out/decline/renounced/kept-hold → "available" (a free agent). NOTE
  //    kept-hold is the DEFAULT held-rights state every visited team's UFA/RFA seeds into
  //    (app.jsx), NOT an affirmative keep — so a held FA stays signable, identical to an
  //    undecided one (the only difference is whether you happened to visit that team).
  // Each team is read through selectTeamView — the canonical merge of the shared options
  // layer + that team's active-mode bucket (the same effective view derivedByTeam uses,
  // and the ONLY layer where opt-in/opt-out/exercise/decline actually live).
  function readWorld(state) {
    const signedTo = {}, optBy = {};
    if (!state || typeof window === "undefined" || typeof window.getRosters !== "function") return { signedTo, optBy };
    const rosters = window.getRosters(state) || {};
    const sc = (typeof window.curSc === "function") ? window.curSc(state) : null;
    const options = (sc && sc.options) || {};
    // Iterate every team with ANY decision — roster buckets OR the shared options layer.
    // A cross-team opt-out the user just made from the hub lands in options[code] for a
    // team that may have no roster bucket yet, so rosters keys alone would miss it.
    const codes = new Set([...Object.keys(rosters), ...Object.keys(options)]);
    const view = (typeof window.selectTeamView === "function") ? window.selectTeamView : null;
    for (const code of codes) {
      let decs = {}, adds = [], md = null;
      if (view) { const tv = view(state, code) || {}; decs = tv.decisions || {}; adds = tv.additions || []; md = tv.mode; }
      else { const modes = rosters[code] || {}; md = Object.keys(modes)[0] || null; const b = (md && modes[md]) || {}; decs = b.decisions || {}; adds = b.additions || []; }
      adds.forEach(a => { if (a && a.name && !signedTo[a.name]) signedTo[a.name] = { team: code, mode: md, kind: "add", id: a.id, fromTrade: !!a._fromTrade }; });
      for (const nm of Object.keys(decs)) {
        const k = decs[nm] && decs[nm].kind;
        if (!k) continue;
        if (k === "signed") { if (!signedTo[nm]) signedTo[nm] = { team: code, mode: md, kind: "resign", name: nm }; }
        // [§4.5 one-world] a traded-away player is on the destination team now — grey them (no
        // FA undo); without this they'd fall through to a SIGNABLE row (fail-open).
        else if (k === "traded") { if (!signedTo[nm]) signedTo[nm] = { team: (decs[nm] && decs[nm].destCode) || code, kind: "traded", name: nm }; }
        else if (k === "opt-in" || k === "exercise") { const oid = code + ":" + nm; if (!optBy[oid]) optBy[oid] = { team: code, status: "staying", kind: k }; }
        else if (k === "opt-out" || k === "decline" || k === "renounced" || k === "kept-hold") { const oid = code + ":" + nm; if (!optBy[oid]) optBy[oid] = { team: code, status: "available", kind: k }; }
      }
    }
    return { signedTo, optBy };
  }

  function Row({ x, onPick, onUndo, onSetOption, capMode }) {
    const [confirming, setConfirming] = useState(false);
    const [opting, setOpting] = useState(false);
    if (x.signedTo) {
      const tName = teamName2(x.signedTo.team);
      // [§4.5 one-world] a TRADED player (or a trade-acquired addition, `fromTrade`) is NOT a user
      // FA-signing — its undo lives in the Trade Machine. Render greyed with a static "trade" hint
      // and NO trash-undo, so the hub can't orphan the trade by removing only the dest addition.
      const isTrade = x.signedTo.kind === "traded" || x.signedTo.fromTrade;
      const subText = x.signedTo.kind === "traded" ? "Traded to " + tName : "Signed to " + tName;
      return (
        <div className={"fahub-row fahub-signed has-logo" + (confirming ? " confirming" : "")}>
          <span className="fahub-photowrap"><Photo nbaId={x.nbaId} name={x.nm} /></span>
          <span className="fahub-rinfo">
            <span className="fahub-nameline"><b>{x.nm}</b><span className="offer-pos">{x.pos}</span></span>
            {confirming
              ? <span className="fahub-subline fahub-undo-q">Undo the {tName} signing of {x.nm}?</span>
              : <span className="fahub-subline fahub-signed-sub">{subText}</span>}
          </span>
          {x.team && <span className="fahub-logo-cell"><Logo code={x.team} /></span>}
          <span className="fahub-sign-cell">
            {isTrade ? (
              <span className="fahub-trade-hint" title="Acquired via trade — undo from the Trade Machine">trade</span>
            ) : confirming ? (
              <span className="fahub-undo-actions">
                <button type="button" className="fahub-undo-no" onClick={() => setConfirming(false)}>Cancel</button>
                <button type="button" className="fahub-undo-yes" onClick={() => { onUndo && onUndo(x); setConfirming(false); }}>Undo</button>
              </span>
            ) : (
              <button type="button" className="fahub-undo-btn" title="Undo signing" onClick={() => setConfirming(true)}><Icon.Trash /></button>
            )}
          </span>
        </div>
      );
    }
    // [Job 3 / one-world] a LEAGUE terminal option (TO/PO): signability follows the SOURCE
    // team's decision. opt-out/decline (available) falls through to the signable row below;
    // staying (opt-in/exercise/kept) → greyed "Staying with X"; undecided → "Pending …".
    // Either way a pencil opens the same 2-way toggle the native decide-flow uses, so "god"
    // can resolve the option inline (no page flip) and THEN sign.
    const leagueOpt = !x.own && (x.status === "team_option" || x.status === "player_option");
    const optStaying = leagueOpt && x.optBy && x.optBy.status === "staying";
    const optPending = leagueOpt && !x.optBy;
    if (optStaying || optPending) {
      const isPO = x.status === "player_option";
      const tName = teamName2(x.team);
      const inKind = isPO ? "opt-in" : "exercise", outKind = isPO ? "opt-out" : "decline";
      const inLbl = isPO ? "Opts in" : "Exercise", outLbl = isPO ? "Opts out" : "Decline";
      const cur = x.optBy && x.optBy.kind;
      return (
        <div className={"fahub-row fahub-opt has-logo" + (optStaying ? " fahub-staying" : " fahub-pending") + (opting ? " opting" : "")}>
          <span className="fahub-photowrap"><Photo nbaId={x.nbaId} name={x.nm} /></span>
          <span className="fahub-rinfo">
            <span className="fahub-nameline">
              <b>{x.nm}</b><span className="offer-pos">{x.pos}</span>
              <span className={"fa-tag st " + x.status.toLowerCase()}>{STLABEL[x.status] || x.status}</span>
            </span>
            {opting
              ? <span className="fahub-subline fahub-opt-q">{isPO ? "Does " + x.nm + " opt in, or out?" : tName + ": exercise " + x.nm + "’s option, or decline?"}</span>
              : <span className="fahub-subline fahub-opt-sub">{optStaying ? "Staying with " + tName : "Pending " + tName + "’s option decision"}</span>}
          </span>
          <span className="fahub-logo-cell"><Logo code={x.team} /></span>
          <span className="fahub-sign-cell">
            {opting ? (
              <span className="fahub-opt-actions">
                <span className="decisions equal two fahub-opt-toggle">
                  <button type="button" className={cur === inKind ? "on-pick" : ""} onClick={() => { onSetOption && onSetOption(x, inKind); setOpting(false); }}><span className="dec-label">{inLbl}</span></button>
                  <button type="button" className={cur === outKind ? "on-neutral" : ""} onClick={() => { onSetOption && onSetOption(x, outKind); setOpting(false); }}><span className="dec-label">{outLbl}</span></button>
                </span>
                <button type="button" className="fahub-opt-cancel" title="Cancel" onClick={() => setOpting(false)}><Icon.X /></button>
              </span>
            ) : (
              <button type="button" className="fahub-opt-btn" title="Set the option decision" onClick={() => setOpting(true)}><Icon.Pencil /></button>
            )}
          </span>
        </div>
      );
    }
    // Chips render in TWO slots: inline after the name (desktop) and beside the
    // "prev. $" subline (phone ≤600px, .fahub-tags-sub) — on narrow rows the
    // unshrinkable chips + Sign cell were crushing the name to "Lu…". CSS swaps
    // visibility via display:none, which also removes the hidden slot from the
    // accessibility tree — so exactly one slot is ever read (no aria-hidden:
    // that hid the chips from screen readers entirely on phones).
    const tags = <>{x.own && x.bird !== "none" && <span className="fa-tag bird">{x.bird}</span>}<span className={"fa-tag st " + x.status.toLowerCase()}>{STLABEL[x.status] || x.status}</span></>;
    return (
      <button className={"fahub-row" + (x.own ? "" : " has-logo")} onClick={() => onPick(x.id)}>
        <span className="fahub-photowrap"><Photo nbaId={x.nbaId} name={x.nm} /></span>
        <span className="fahub-rinfo">
          <span className="fahub-nameline">
            <b>{x.nm}</b><span className="offer-pos">{x.pos}</span>
            <span className="fahub-tags fahub-tags-top">{tags}</span>
          </span>
          <span className="fahub-line2">
            <span className="fahub-subline">{x.prior > 0 ? "prev. " + m(x.prior) : "no prior salary"}{x.own && x.hold > 0 && capMode ? " · hold " + m(x.hold) : ""}</span>
            <span className="fahub-tags fahub-tags-sub">{tags}</span>
          </span>
        </span>
        {!x.own && <span className="fahub-logo-cell"><Logo code={x.team} /></span>}
        <span className="fahub-sign-cell"><span className="offer-ract">{x.own ? "Re-sign" : "Sign"}</span></span>
      </button>
    );
  }

  function OfferSheet({ allTeams, teamCode, teamName, nbaIdByName, teams = [], belowCap = false, capSpace = 32.5, ownOnly = false, dispatch, state, capBase, apronTotal, unlikely, additions }) {
    const [tab, setTab] = useState(belowCap ? "league" : "own");
    const [q, setQ] = useState("");
    const [editId, setEditId] = useState(null);
    const [salaryStr, setSalary] = useState("8.0");
    const [years, setYears] = useState(2);
    const [raise, setRaise] = useState(0.08);
    const [raises, setRaises] = useState([]);
    const [pyExpanded, setPyExpanded] = useState(false);
    const [option, setOption] = useState(null);
    const [method, setMethod] = useState(belowCap ? "cap" : "ntmle");
    const [teamFilter, setTeamFilter] = useState("");
    const [statusSel, setStatusSel] = useState(ALL_ST);   // league status multi-select (default: all)
    const [statusOpen, setStatusOpen] = useState(false);
    const [teamOpen, setTeamOpen] = useState(false);

    const fas = buildFAs(allTeams, teamCode, nbaIdByName);
    const world = readWorld(state);
    fas.forEach(f => { f.signedTo = world.signedTo[f.nm] || null; f.optBy = world.optBy[f.id] || null; });
    // your own re-sign drops into the roster → it vanishes from the panel; every OTHER signing shows greyed with an undo.
    const vanished = f => f.signedTo && f.signedTo.kind === "resign" && f.signedTo.team === teamCode;
    const own = fas.filter(f => f.own && !vanished(f)), league = fas.filter(f => !f.own && !vanished(f));
    const capMode = !!(state && state.mode === "cap");   // G: cap-hold numbers only in cap-space mode
    const leagueTeams = [...new Set(league.map(x => x.team))].sort((a, b) => teamName2(a).localeCompare(teamName2(b)));
    // belowCap shows league FAs only; renounced own players will be appended here once the renounce→cap-space engine wiring lands.
    const src = belowCap ? league : (ownOnly ? own : (tab === "own" ? own : league));
    const ql = q.toLowerCase();
    const allSt = statusSel.length === ALL_ST.length;
    const matched = src.filter(x => x.nm.toLowerCase().includes(ql) && (!teamFilter || x.team === teamFilter) && (tab === "own" || statusSel.includes(x.status)));
    const CAP = tab === "league" && !q ? 60 : 400;
    const list = matched.slice(0, CAP);
    const teamLbl = teamName || teamCode || "Yours";
    const statusSummary = allSt ? "All types" : statusSel.length === 0 ? "No types" : ALL_ST.filter(v => statusSel.includes(v)).map(v => STLABEL[v]).join(" · ");
    const teamMenuCurrent = teams.find(t => t.code === teamCode) || { code: teamCode };

    const p = fas.find(x => x.id === editId);
    const sal = parseFloat(salaryStr) || 0;
    const isBird = p && p.own && (p.bird === "Bird" || p.bird === "Early-Bird");
    const maxRate = isBird ? 0.08 : 0.05;
    // [J1a] real numbers from the engine: capBase/apronTotal arrive in raw dollars; OfferSheet works in $M.
    const capBaseM = Number.isFinite(capBase) ? capBase / 1e6 : null;
    const apronTotalM = Number.isFinite(apronTotal) ? apronTotal / 1e6 : null;
    // [J1a fix] BASE drives the apron TIER (which exception) + the apron-crossing projection, so it must be the
    // holds-FREE Apron Team Salary (apronTotal), NOT the holds-inclusive capBase. Only capRoom (the cap-space
    // ceiling) uses capBase. Mirrors eligibleSigningTools: room <- capBase, apron tier <- apronTotal.
    const BASE = apronTotalM != null ? apronTotalM : BASE_MOCK;      // real holds-free apron salary ($M); mock fallback
    const capRoom = capBaseM != null ? Math.max(0, (C.cap || 165e6) / 1e6 - capBaseM) : capSpace;  // real cap room ($M); mock fallback
    const teamTier = BASE < apron1 ? "nonTax" : BASE < apron2 ? "tax" : "apron2plus";
    // Vet-Min size is per-player (YOS-scaled), not the flat TOOLS.min mock; pass `who` so
    // openRow can size it for the row being opened (where `p`/editId isn't set yet).
    const effMax = (id, who = p) => (belowCap && id === "cap") ? capRoom
      : (id === "min") ? ((who && minForYosM(who.yos) != null) ? minForYosM(who.yos) : (C.vetMinCapCharge ? C.vetMinCapCharge / 1e6 : TOOLS.min.max))   // null-YOS → the 2-YOS min ($2.45M), not the flat $3.0M mock
      : TOOLS[id].max;
    const methods = belowCap ? ["cap", "room"] : ((typeof eligibleSigningTools === "function") ? eligibleSigningTools(capBase, apronTotal).map(n => KEY_BY_SOURCE[n]).filter(Boolean) : METHODS_BY_TIER[teamTier]);
    const sel = (p && !p.own && method && TOOLS[method]) ? TOOLS[method] : null;
    const maxYears = (p && p.own) ? (isBird ? 5 : 4) : (sel ? sel.y : 2);
    const base = (typeof parseSalaryFromMField === "function" ? parseSalaryFromMField(salaryStr) : sal * 1e6) || 0;
    const calc = window.contractCalc(base, years, { adjustRaises: true, raisePct: raise, raises, pyExpanded, maxRate });
    // [J1b] Real legality verdict from the engine — mirrors the native SignFAModal (league signs)
    // and reSignBlocked (own re-signs). DISPLAY ONLY this step: commitSign is NOT gated yet (J2 does that).
    // Engine works in raw dollars (base, room, ceilings); OfferSheet's `prior`/room are $M, so convert.
    const sourceTool = !p ? null : (p.own ? "Cap space" : (SOURCE_BY_METHOD[method] || null));
    // Hard cap = stricter of any PRIOR stored hard cap on this team and the chosen tool's self-imposed apron ceiling.
    const _scHC = (typeof window.curSc === "function") ? window.curSc(state) : null;
    const _storedAt = (_scHC && typeof window.strictestHardCap === "function") ? window.strictestHardCap((_scHC.hardCaps || {})[teamCode]) : null;
    const _priorCeil = _storedAt === "firstApron" ? C.apron1 : _storedAt === "secondApron" ? C.apron2 : Infinity;
    const _toolCeil = (sourceTool === "MLE" || sourceTool === "BAE") ? C.apron1 : sourceTool === "TPMLE" ? C.apron2 : Infinity;
    const _hc = Math.min(_priorCeil, _toolCeil);
    const hardCapCeiling = Number.isFinite(_hc) ? _hc : null;
    const _ownInApron = !!(p && p.own && (p.status === "team_option" || p.status === "player_option" || p.status === "RFA"));
    const apronAfter = (!Number.isFinite(apronTotal) || _ownInApron) ? null : (apronTotal + (Number(unlikely) || 0) + base);
    const TOOL_KEY = { "MLE": "nt_mle", "TPMLE": "tp_mle", "BAE": "bae", "Room MLE": "room_mle" };
    const _adds = Array.isArray(additions) ? additions : [];
    const _excRem = (typeof exceptionRemainingFor === "function" && sourceTool) ? exceptionRemainingFor(_adds, sourceTool) : null;
    const _usedExc = [...(_adds.map(a => TOOL_KEY[a.source]).filter(Boolean)), TOOL_KEY[sourceTool]].filter(Boolean);
    const _legalArgs = !p ? null : (p.own ? {
        birdStatus: (typeof birdRightsToStatus === "function") ? birdRightsToStatus(p.birdRights) : "bird",
        instrument: "new", ownReSign: (((p.prior || 0) > 0) || (p.yos != null)), tool: "Cap space",
        yos: p.yos, priorSalary: (p.prior || 0) * 1e6, higherMax: !!p.designated, minScale: (C.minScale || null),
        years, startingSalary: base, raisePct: pyExpanded ? null : raise, raises: pyExpanded ? raises.slice(0, Math.max(0, years - 1)) : null,
        hardCapCeiling, apronAfter,
      } : {
        birdStatus: "other", instrument: "new", tool: sourceTool || "Cap space",
        yos: p.yos, priorSalary: (p.prior || 0) * 1e6, minScale: (C.minScale || null),
        years, startingSalary: base, raisePct: pyExpanded ? null : raise, raises: pyExpanded ? raises.slice(0, Math.max(0, years - 1)) : null,
        exceptionRemaining: _excRem ? _excRem.left : null,
        availableRoom: (sourceTool === "Cap space" && Number.isFinite(capBase)) ? Math.max(0, (C.cap || 165e6) - capBase) : null,
        usedExceptions: _usedExc, hardCapCeiling, apronAfter,
      });
    const _lx = (_legalArgs && typeof signingLegality === "function") ? signingLegality(_legalArgs) : { violations: [], blocked: false };
    const _blockV = _lx.violations.find(v => v.severity === "block");
    const verdict = !p ? { ok: false, why: "" }
      : (!p.own && !sel) ? { ok: false, why: "pick a signing method" }
      : { ok: !_lx.blocked, why: _blockV ? _blockV.msg : "" };
    const openRow = id => {
      const x = fas.find(f => f.id === id) || {};
      const xBird = x.own && (x.bird === "Bird" || x.bird === "Early-Bird");
      const dMethod = belowCap ? "cap" : (methods[0] || METHODS_BY_TIER[teamTier][0]);
      // default first-year salary = previous year's, but capped at the most we can legally give them (no prior → the method/room max)
      let defSal = x.own ? (x.prior || 8) : Math.min(x.prior || effMax(dMethod, x), effMax(dMethod, x));
      const _xMin = minForYosM(x.yos);   // never default BELOW the player's YOS min (so the editor doesn't open in a min-floor block). Round the min UP to 0.1M so the 0.1M-floored display stays ≥ the exact min; own uncapped, league capped at the tool ceiling.
      if (_xMin != null && defSal < _xMin) { const _up = Math.ceil(_xMin * 10) / 10; defSal = x.own ? _up : Math.min(_up, effMax(dMethod, x)); }
      setEditId(id); setSalary((Math.floor(defSal * 10) / 10).toFixed(1)); setYears(2);
      setRaise(xBird ? 0.08 : 0.05); setRaises([]); setPyExpanded(false);
      setOption(null); setMethod(dMethod);
    };
    const commitSign = () => {
      if (!p || typeof dispatch !== "function") return;
      const n = base;                                   // raw dollars (parseSalaryFromMField)
      if (!n || n <= 0) return;
      // [J2] Hard-block illegal signings (real engine verdict), mirroring the native paths
      // (reSignBlocked / SignFAModal): refuse + toast the reason instead of committing.
      if (!verdict.ok) {
        dispatch({ type: "SET_TOAST", toast: "Can't sign " + p.nm + " — " + (verdict.why || "this signing isn't legal.") });
        return;
      }
      const yrs = years;
      if (p.own) {
        // own re-sign → SET_DECISION (mirrors the native cap-holds re-sign editor)
        const dec = { kind: "signed", salary: n, years: yrs };
        if (yrs > 1) { dec.option = option || null; if (pyExpanded) dec.raises = raises.slice(0, yrs - 1); else dec.raisePct = raise; }
        dispatch({ type: "SET_DECISION", player: p.nm, decision: dec });
        dispatch({ type: "SET_TOAST", toast: "Re-signed " + p.nm + " · " + yrs + "y · " + fmt$(n) });
      } else {
        // league sign → ADD_PLAYER (mirrors the native Sign-FA modal; `source` must be the engine string)
        const source = SOURCE_BY_METHOD[method] || "Cap space";
        // §6(h): a vet-min contract IS the exact (odd-dollar) YOS minimum — snap the committed
        // salary to it so the 0.1M-floored editor value isn't paid/charged BELOW the real
        // minimum (e.g. a 2-YOS player whose floored 2.4M < the true 2.45M min).
        const _exactMin = (source === "Vet min") ? minForYosRaw(p.yos) : null;
        const sal = (_exactMin != null) ? _exactMin : n;
        const player = { id: "fa-" + Date.now(), name: p.nm, position: p.pos, salary: sal,
          years: yrs, option: yrs > 1 ? (option || null) : null, source, nbaId: p.nbaId || null, yos: (p.yos != null ? p.yos : null) };
        if (yrs > 1) { if (pyExpanded) player.raises = raises.slice(0, yrs - 1); else player.raisePct = raise; }
        dispatch({ type: "ADD_PLAYER", player });
        if (HARDCAP_AT[source]) dispatch({ type: "SET_HARD_CAP", team: teamCode, at: HARDCAP_AT[source], source, signId: player.id });
        dispatch({ type: "SET_TOAST", toast: "Signed " + p.nm + " · " + yrs + "y · " + fmt$(sal) });
      }
      setEditId(null);
    };
    const onUndo = x => {
      if (!x || !x.signedTo || typeof dispatch !== "function") return;
      const s = x.signedTo;
      dispatch({ type: "UNDO_SIGNING", team: s.team, mode: s.mode, kind: s.kind, id: s.id, name: s.name || x.nm });
    };
    // [Job 3 / one-world] resolve ANOTHER team's terminal option inline — "god" opting a
    // cross-team player in/out, persisted on THAT team (state.jsx SET_DECISION `team`).
    // opt-out/decline → the player becomes a signable FA; opt-in/exercise → he stays put.
    const resolveOption = (x, kind) => {
      if (!x || !kind || typeof dispatch !== "function") return;
      dispatch({ type: "SET_DECISION", team: x.team, player: x.nm, decision: { kind } });
      const free = kind === "opt-out" || kind === "decline";
      dispatch({ type: "SET_TOAST", toast: free
        ? x.nm + " is now a free agent — " + teamName2(x.team) + " let him go."
        : x.nm + " stays with " + teamName2(x.team) + "." });
    };
    const salaryNode = (
      <>
        <span className="salary-edit inline-final">
          <span className="pre">$</span>
          <input autoFocus type="text" inputMode="decimal" enterKeyHint="done" value={salaryStr}
                 onChange={e => setSalary(e.target.value)} onFocus={e => e.target.select()}
                 onKeyDown={e => { if (e.key === "Escape") setEditId(null); }} />
          {(typeof mFieldShowM !== "function" || mFieldShowM(salaryStr)) && <span className="pre" style={{ fontSize: 12 }}>M</span>}
        </span>
        {p && p.prior > 0 && <span className="fahub-salprev">prev. {fmt$(p.prior * 1e6)}</span>}
      </>
    );
    const renderEditor = x => {
      // "max first-year $X" is intentionally omitted until the real max-salary engine is wired here.
      const refEl = !x.own ? <Logo code={x.team} /> : null;
      const footerText = x.own
        ? (verdict.ok ? "Re-signs to a new contract." : (verdict.why || "This re-signing isn't legal."))
        : verdict.ok
          ? "Signs via " + (sel ? sel.n : "exception") + " · " + years + "y · " + fmt$(calc.total) + " total"
          : (verdict.why || "No method fits — cut salary or years");
      return (
        <div key={x.id} className="fahub-row expanded fahub-edit flex-row body-row cap-row editing resign-edit cap-grid">
          <EditorPhoto nbaId={x.nbaId} name={x.nm} />
          <div className="info-col">
            <div className="cap-line1">
              <div className="player-name">{x.nm}{x.pos && <span className="pos-name">{x.pos}</span>}</div>
              {refEl}
            </div>
            <div className="fahub-edit-meta">
              <span className={"fa-tag st " + x.status.toLowerCase()}>{STLABEL[x.status] || x.status}</span>
            </div>
            {x.own && (
              <div className="cap-moves-actions">
                <div className="decisions equal two apron-resign-choice">
                  <button className="on-yes"><Icon.Check className="dec-icon" /><span className="dec-label">Signed</span></button>
                  <button onClick={() => setEditId(null)}><span className="dec-label">Don't re-sign</span></button>
                </div>
              </div>
            )}
            {!x.own && (
              <div className="cap-moves-actions fahub-methodwrap">
                <div className="fahub-method-lbl">Sign via</div>
                <div className={"decisions equal fahub-method " + (methods.length === 3 ? "three" : methods.length === 2 ? "two" : "one")}>
                  {methods.map(mid => {
                    const T = TOOLS[mid];
                    return (
                      <button key={mid} className={method === mid ? "on-pick" : ""} onClick={() => { setMethod(mid); if (!(belowCap && mid === "cap")) setSalary((Math.floor(effMax(mid) * 10) / 10).toFixed(1)); }}>
                        <span className="dec-label">{T.n}<span className="fahub-method-max"> {m(effMax(mid))}</span></span>
                      </button>
                    );
                  })}
                </div>
              </div>
            )}
          </div>
          <window.ContractEditorCard salaryNode={salaryNode} base={base} years={years} setYears={setYears}
            maxYears={maxYears} maxRate={maxRate} calc={calc} setRaisePct={setRaise} setRaises={setRaises}
            setPyExpanded={setPyExpanded} option={option} setOption={setOption} />
          <window.ContractEditorFooter
            text={footerText}
            tone={verdict.ok ? "info" : "warn"}>
            <window.ConfirmButtons onSave={commitSign} onCancel={() => setEditId(null)} saveTitle={x.own ? "Re-sign" : "Sign"} />
          </window.ContractEditorFooter>
        </div>
      );
    };

    return (
      <section className={"section fa-hub" + (belowCap ? " fa-hub-capspace" : "")} id={belowCap ? "fa-capspace-hub" : "fa-signing-hub"}>
        <style>{OFF6_CSS}</style>
        <div className="fahub-head">
          <div className="fahub-title">{belowCap ? "Free agents" : (ownOnly ? "Re-signings" : "Free agents")}</div>
          <div className="fahub-sub">{belowCap ? ("Cap space " + m(capRoom) + " · Room MLE " + m(TOOLS.room.max)) : (ownOnly ? (own.length + " to re-sign") : (own.length + " " + teamCode + ", " + league.length + " others"))}</div>
          {!belowCap && !ownOnly && (<div className="decisions equal fahub-toggle">
            <button className={tab === "own" ? "on-pick" : ""} onClick={() => { setTab("own"); setQ(""); setTeamFilter(""); setEditId(null); setStatusOpen(false); setTeamOpen(false); }}>{teamCode}</button>
            <button className={tab === "league" ? "on-pick" : ""} onClick={() => { setTab("league"); setQ(""); setTeamFilter(""); setEditId(null); setStatusOpen(false); setTeamOpen(false); }}>Others</button>
          </div>)}
        </div>
        {tab === "league" && (
          <div className="fahub-controls">
            <input className="offer-q" placeholder="Search free agents…" value={q} onChange={e => setQ(e.target.value)} />
            <div className="fahub-pickwrap fahub-statuswrap">
              <button className={"fahub-trigger" + (statusOpen ? " open" : "") + (!allSt ? " filtered" : "")} onClick={() => { setStatusOpen(v => !v); setTeamOpen(false); }} title="Filter by status">
                <span className="fahub-trigtxt">{statusSummary}</span>
                <span className="fahub-caret">{statusOpen ? "✓" : "▾"}</span>
              </button>
              {statusOpen && (
                <>
                  <button type="button" className="fahub-scrim" aria-label="Close status filter" onMouseDown={e => { e.preventDefault(); e.stopPropagation(); }} onClick={() => setStatusOpen(false)} />
                  <div className="fahub-pop statuspop">
                    <div className="fahub-pop-head">Show types</div>
                    <button className={"fahub-stopt all" + (allSt ? " on" : "")} onClick={() => setStatusSel(ALL_ST)}>
                      <span className="fahub-stcheck">{allSt ? "✓" : ""}</span><span className="fahub-stname">All types</span>
                    </button>
                    {ST_OPTS.map(([val, lbl]) => {
                      const on = statusSel.includes(val);
                      return (
                        <button key={val} className={"fahub-stopt" + (on ? " on" : "")} onClick={() => setStatusSel(s => on ? s.filter(v => v !== val) : ALL_ST.filter(v => s.includes(v) || v === val))}>
                          <span className="fahub-stcheck">{on ? "✓" : ""}</span><span className="fahub-stname">{lbl}</span><span className={"fa-tag st " + val.toLowerCase()}>{STLABEL[val]}</span>
                        </button>
                      );
                    })}
                  </div>
                </>
              )}
            </div>
            <div className="fahub-pickwrap fahub-teamwrap">
              <button className={"fahub-trigger" + (teamOpen ? " open" : "") + (teamFilter ? " filtered" : "")} onClick={() => { setTeamOpen(v => !v); setStatusOpen(false); }} title="Filter by team">
                <span className="fahub-trigtxt">{teamFilter ? teamName2(teamFilter) : "All teams"}</span>
                <span className="fahub-caret">▾</span>
              </button>
              {teamOpen && (
                <>
                  <button type="button" className="fahub-scrim" aria-label="Close team filter" onMouseDown={e => { e.preventDefault(); e.stopPropagation(); }} onClick={() => setTeamOpen(false)} />
                  <div className="fahub-pop teampop">
                    <button className={"fahub-allteams" + (!teamFilter ? " on" : "")} onClick={() => { setTeamFilter(""); setTeamOpen(false); }}>
                      <span className="fahub-stcheck">{!teamFilter ? "✓" : ""}</span><span className="fahub-stname">All teams</span>
                    </button>
                    {typeof window !== "undefined" && window.TeamMenu &&
                      <window.TeamMenu currentTeam={teamMenuCurrent} teams={teams} onPick={t => { setTeamFilter(t.code); setTeamOpen(false); }} onClose={() => setTeamOpen(false)} />}
                  </div>
                </>
              )}
            </div>
          </div>
        )}
        <div className={"fahub-results" + (tab === "league" ? " league" : "")}>
          {list.map(x => editId === x.id
            ? renderEditor(x)
            : <Row key={x.id} x={x} onPick={openRow} onUndo={onUndo} onSetOption={resolveOption} capMode={capMode} />)}
          {!list.length && <div className="offer-empty">No free agents{q ? " match “" + q + "”" : ""}.</div>}
          {matched.length > list.length && <div className="fahub-more">+ {matched.length - list.length} more — search to narrow</div>}
        </div>
      </section>
    );
  }

  const OFF6_CSS = `
.fa-hub{padding:0;overflow:hidden;scroll-margin-top:90px}
.fahub-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:12px 14px}
.fahub-title{font-weight:700;font-size:15px;white-space:nowrap}
.fahub-sub{font-weight:500;font-size:12px;color:var(--text-faint);white-space:nowrap}
.fahub-controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 14px;background:var(--bg-row);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.offer-q{flex:1 1 200px;min-width:120px;height:36px;background:var(--bg-elevated);border:1px solid var(--line);border-radius:8px;padding:0 12px;color:var(--text);font:inherit;font-size:13px;outline:none;transition:border-color .12s}
.offer-q::placeholder{color:var(--text-dim)}
.offer-q:hover{border-color:var(--line-strong)}
.offer-q:focus{background:var(--bg-surface);border-color:var(--brand);box-shadow:0 0 0 2px color-mix(in oklab, var(--brand), transparent 82%)}
.fahub-toggle{flex:0 0 auto;margin-left:auto}
.fahub-toggle button.on-pick{background:var(--brand);border-color:var(--brand);color:#fff}
/* status + team pickers (reuse trade-machine stays-open + scrim mechanics) */
.fahub-pickwrap{position:relative;flex:0 0 auto}
.fahub-teamwrap{margin-left:auto}
.fahub-trigger{display:inline-flex;align-items:center;gap:8px;width:150px;height:36px;background:var(--bg-surface);border:1px solid var(--line);border-radius:7px;padding:0 10px;color:var(--text);font:inherit;font-size:12px;font-weight:600;cursor:pointer;transition:border-color .12s,background .12s}
.fahub-statuswrap .fahub-trigger{width:140px}
.fahub-trigger:hover{background:var(--bg-row-hover);border-color:var(--line-strong)}
.fahub-trigger.open{position:relative;z-index:60;background:var(--bg-row-hover);border-color:var(--line-strong)}
.fahub-trigger.filtered{background:color-mix(in oklab, var(--bg-surface), var(--incoming) 12%);border-color:var(--line);color:var(--text)}
.fahub-trigtxt{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fahub-caret{width:12px;font-size:11px;color:var(--text-faint);flex:0 0 auto}
.fahub-trigger.filtered .fahub-caret{color:var(--incoming)}
@media (max-width:430px){.fahub-title{order:1}.fahub-toggle{order:2}.fahub-sub{order:3;flex:0 0 100%}}
.fahub-scrim{position:fixed;inset:0;z-index:40;background:transparent;border:0;padding:0;cursor:default}
.fahub-pop{position:absolute;top:calc(100% + 6px);z-index:60;background:var(--bg-surface);border:1px solid var(--line-strong);border-radius:12px;box-shadow:var(--shadow-lg);overflow:hidden}
.fahub-pop.statuspop{left:0;min-width:218px;padding:6px}
.fahub-pop.teampop{right:0;width:min(640px,92vw);max-height:min(70vh,470px);overflow:auto;padding:6px}
.fahub-pop-head{font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--text-faint);padding:7px 9px 5px}
.fahub-stopt{display:flex;align-items:center;gap:9px;width:100%;background:transparent;border:1px solid transparent;border-radius:7px;padding:9px;color:var(--text);font:inherit;font-size:13px;cursor:pointer;text-align:left}
.fahub-stopt:hover{background:var(--bg-row-hover);border-color:var(--line)}
.fahub-stopt.on,.fahub-allteams.on{background:color-mix(in oklab, var(--bg-surface), var(--incoming) 12%);border-color:var(--line);color:var(--text)}
.fahub-stopt.on:hover,.fahub-allteams.on:hover{border-color:var(--line-strong)}
.fahub-stopt.all{border-bottom:0;border-radius:0;margin:0 0 4px}
.fahub-stname{flex:1;min-width:0}
.fahub-stcheck{width:14px;flex:0 0 auto;color:var(--incoming);font-weight:700;font-size:10px}
.fahub-pop .fa-tag.st{flex:0 0 auto}
.fahub-allteams{display:flex;align-items:center;gap:9px;width:100%;margin:0 2px 6px;padding:9px 10px;background:var(--bg-row);border:1px solid transparent;border-radius:7px;color:var(--text);font:inherit;font-size:13px;font-weight:700;cursor:pointer;text-align:left}
.fahub-allteams:hover{background:var(--bg-row-hover);border-color:var(--line)}
/* main-site TeamMenu rendered inline inside the team popover */
.fahub-pop .team-menu{position:static;transform:none;inset:auto;top:auto;left:auto;width:100%;max-width:none;max-height:none;box-shadow:none;border:0;border-radius:0;background:transparent;overflow:visible}
.fahub-pop .team-menu-grid{padding:6px 4px 8px}
.fahub-pop .team-menu-cell{padding:10px}
.fahub-pop .team-menu-head{border-top:1px solid var(--line);margin-top:4px;padding-top:10px}
.fahub-results{display:block;overflow:auto}
.fahub-results.league{height:520px}

.fahub-row{display:grid;grid-template-columns:60px minmax(0,1fr) auto;align-items:center;gap:12px;background:transparent;border:0;border-bottom:1px solid var(--line);cursor:pointer;text-align:left;color:inherit;width:100%;padding:7px 0 7px 12px;min-height:64px}
.fahub-row.has-logo{grid-template-columns:60px minmax(0,1fr) auto auto}
.fahub-row:hover{background:var(--bg-row)}
.fahub-photowrap{display:inline-flex;flex:0 0 auto}
.fahub-photo{width:60px;height:60px;border-radius:4px;object-fit:cover;object-position:top center;display:block}
.fahub-photo.placeholder{display:flex;align-items:center;justify-content:center;background:var(--bg-row);font-weight:700;color:var(--text-faint);font-size:14px}
.fahub-rinfo{min-width:0;display:flex;flex-direction:column;gap:3px}
.fahub-nameline{display:flex;align-items:center;gap:6px;min-width:0}
.fahub-nameline b{font-size:14.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.offer-pos{font-size:10px;color:var(--text-faint);font-weight:600}
.fahub-subline{font-size:12px;color:var(--text-faint)}
.fa-tag{font-size:9px;font-weight:700;padding:2px 6px;border-radius:5px;letter-spacing:.2px;flex:0 0 auto}
.fa-tag.bird{background:var(--bg-row);color:var(--text-dim)}
.fa-tag.st{background:var(--bg-row);color:var(--text-dim)}
.fa-tag.st.rfa,.fa-tag.st.team_option,.fa-tag.st.player_option{background:var(--bg-row);color:var(--text-dim)}
.fahub-tags{display:inline-flex;align-items:center;gap:6px;flex:0 0 auto}
.fahub-line2{display:flex;align-items:center;gap:8px;min-width:0}
.fahub-tags-sub{display:none}
.fahub-logo-cell{align-self:stretch;display:flex;align-items:center;justify-content:center}
.fahub-logo-img{width:34px;height:34px;object-fit:contain;display:block}
.fahub-sign-cell{align-self:stretch;display:flex;align-items:center;justify-content:flex-end;border-left:1px solid var(--line);padding-left:14px;padding-right:12px}
.offer-ract{font-size:12px;font-weight:700;padding:8px 16px;border-radius:8px;background:var(--pos);color:#fff;border:0}
@media(min-width:780px){.fahub-sign-cell{padding-left:22px;padding-right:16px}.fahub-logo-cell{padding-right:6px}}
/* Phone: chips leave the name line (where they + the Sign cell crushed long names
   to "Lu…") and drop to the second line beside "prev. $"; the name keeps the full
   first line. Same 600px tier as the hub's other row mobile rules. NOTE: this
   block must stay AFTER the base .fahub-sign-cell/.offer-ract rules — equal
   specificity, source order decides (Codex retro finding 5). */
@media (max-width:600px){
  .fahub-row .fahub-tags-top{display:none}
  .fahub-row .fahub-tags-sub{display:inline-flex}
  .fahub-line2{flex-wrap:wrap;row-gap:2px;gap:6px}
  /* reclaim row width so sub + chips share one line even with wide chips
     (Non-Bird + UFA needs ~173px): slimmer Sign cell + row padding/gaps +
     tighter pills. flex-wrap above stays as the safety valve for the genuinely
     long cap-mode "prev. $X · hold $Y" sublines — wrap, never truncate. */
  .fahub-row{padding-left:8px;gap:8px}
  .fahub-sign-cell{padding-left:8px;padding-right:8px}
  .offer-ract{padding:8px 12px}
  .fahub-tags-sub .fa-tag{padding:2px 4px;letter-spacing:0}
}

/* expanded editor uses the native apron re-sign grid; FA-specific rules keep the
   contract card/footer full width and the fallback photo as a full column strip. */
.fahub-row.expanded.resign-edit{--row-h:64px;--photo-ext:1.65;--fahub-editor-gap:14px;grid-template-columns:var(--row-h) minmax(0,1fr);column-gap:var(--fahub-editor-gap);row-gap:1px;padding:11px 14px;cursor:default}
.fahub-row.expanded.resign-edit.cap-grid{--photo-ext:1.65}
.fahub-row.expanded.resign-edit > .row-photo{height:100%;align-self:stretch;object-fit:cover;object-position:top center}
.fahub-row.expanded.resign-edit > .row-photo.placeholder{width:100%;height:100%;align-self:stretch}
.fahub-row.expanded.resign-edit .cap-line1{align-items:center}
.fahub-row.expanded.resign-edit .cap-line1{justify-content:flex-start}
.fahub-row.expanded.resign-edit .cap-line1 .fahub-logo-img{margin-left:auto}
.fahub-row.expanded.resign-edit > .info-col .cap-moves-actions{margin-top:auto}
.fahub-row.expanded.resign-edit > .info-col{padding-top:0;justify-content:flex-start;gap:0}
.fahub-edit-meta{order:2;display:flex;align-items:center;width:100%;min-height:16px;margin-top:5px}
.fahub-row.expanded.resign-edit > .contract-editor-card{grid-column:1 / -1;width:100%;max-width:none;--contract-label-w:var(--row-h);--fahub-card-gap:var(--fahub-editor-gap);padding-left:0}
.fahub-row.expanded.resign-edit .contract-editor-row{column-gap:var(--fahub-card-gap)}
.fahub-row.expanded.resign-edit .contract-editor-label{text-align:right}
.fahub-row.expanded.resign-edit > .contract-editor-footer{grid-column:1 / -1;width:100%;max-width:none}
.fahub-row.expanded.resign-edit > .contract-editor-footer.resign-bar{display:grid;grid-template-columns:var(--row-h) minmax(0,1fr);column-gap:var(--fahub-editor-gap)}
.fahub-methodwrap{display:flex;flex-direction:column;gap:5px;width:100%}
.fahub-method-lbl{font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--text-faint)}
.fahub-method-max{font-weight:600;color:var(--text-faint)}
.fahub-method button.on-pick{background:var(--pos);border-color:var(--pos);color:#fff}
.fahub-method button.on-pick .fahub-method-max{color:#fff;opacity:.85}
.fahub-salprev{margin-left:10px;font-size:11px;color:var(--text-faint);white-space:nowrap}
/* Phone: native card padding/label tweaks + let the header line wrap. */
@media (max-width: 600px) {
  .fahub-row.expanded.resign-edit { --row-h: 62px; --fahub-editor-gap: 7px; }
  .fahub-row.expanded.resign-edit .contract-editor-card { --contract-label-w: var(--row-h); gap: 8px; padding: 9px 8px 9px 0; }
  .fahub-row.expanded.resign-edit .contract-editor-label { font-size: 9.5px; }
  .fahub-row.expanded.resign-edit .cap-line1 { flex-wrap: wrap; justify-content: flex-start; row-gap: 2px; }
}
.offer-empty{color:var(--text-faint);font-size:13px;padding:16px}
.fahub-more{color:var(--text-faint);font-size:12px;padding:12px;text-align:center}
/* one-world: a player already signed (by any team) shows greyed with an undo */
.fahub-row.fahub-signed{opacity:.6;cursor:default}
.fahub-row.fahub-signed.confirming{opacity:1;background:var(--bg-row)}
.fahub-signed-sub{color:var(--text-faint);font-style:italic}
.fahub-undo-q{color:var(--text);font-weight:600}
.fahub-undo-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:8px;background:transparent;border:1px solid var(--line);color:var(--text-faint);cursor:pointer;transition:background .12s,border-color .12s,color .12s}
.fahub-trade-hint{font-size:9px;font-weight:700;letter-spacing:.4px;text-transform:uppercase;color:var(--text-faint);background:var(--bg-row);border:1px solid var(--line);border-radius:5px;padding:3px 7px;white-space:nowrap}
.fahub-undo-btn:hover{background:var(--bg-row-hover);border-color:var(--danger);color:var(--danger)}
.fahub-undo-actions{display:inline-flex;gap:6px;align-items:center}
.fahub-undo-no{font-size:12px;font-weight:600;padding:7px 12px;border-radius:8px;background:transparent;border:1px solid var(--line);color:var(--text);cursor:pointer}
.fahub-undo-yes{font-size:12px;font-weight:700;padding:7px 14px;border-radius:8px;background:var(--danger);border:1px solid var(--danger);color:#fff;cursor:pointer}
/* one-world: a LEAGUE option player whose availability hangs on the SOURCE team's decision —
   greyed "Staying with X" / "Pending X's option" with an inline pencil that opens the same
   opt-in/out toggle the native decide-flow uses (god resolves it inline, then signs). */
.fahub-row.fahub-opt{cursor:default}
.fahub-row.fahub-opt.fahub-staying{opacity:.6}
.fahub-row.fahub-opt.opting{opacity:1;background:var(--bg-row)}
.fahub-opt-sub{color:var(--text-faint);font-style:italic}
.fahub-opt-q{color:var(--text);font-weight:600}
.fahub-opt-btn{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:8px;background:transparent;border:1px solid var(--line);color:var(--text-faint);cursor:pointer;transition:background .12s,border-color .12s,color .12s}
.fahub-opt-btn:hover{background:var(--bg-row-hover);border-color:var(--brand);color:var(--brand)}
.fahub-opt-btn svg{width:16px;height:16px}
.fahub-opt-actions{display:inline-flex;gap:6px;align-items:center;justify-content:flex-end}
.fahub-opt-toggle{width:auto;display:inline-grid;grid-auto-flow:column}
.fahub-opt-toggle button{font-size:11px;font-weight:700;padding:7px 12px;white-space:nowrap}
.fahub-opt-cancel{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:8px;background:transparent;border:1px solid var(--line);color:var(--text-faint);cursor:pointer;flex:0 0 auto}
.fahub-opt-cancel:hover{background:var(--bg-row-hover);border-color:var(--line-strong);color:var(--text)}
.fahub-opt-cancel svg{width:14px;height:14px}
/* phone: the opened toggle reflows onto its OWN full-width line so it never overflows a
   narrow row (Finding 4); name + logo stay on the top line. */
@media (max-width:600px){
  .fahub-row.fahub-opt .fahub-opt-q,.fahub-row.fahub-opt .fahub-opt-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .fahub-row.fahub-opt.opting{grid-template-columns:60px minmax(0,1fr) auto;grid-template-areas:"photo info logo" "act act act";row-gap:7px}
  .fahub-row.fahub-opt.opting>.fahub-photowrap{grid-area:photo}
  .fahub-row.fahub-opt.opting>.fahub-rinfo{grid-area:info}
  .fahub-row.fahub-opt.opting>.fahub-logo-cell{grid-area:logo}
  .fahub-row.fahub-opt.opting>.fahub-sign-cell{grid-area:act;justify-content:flex-end;border-left:0;padding:0 4px 4px}
  .fahub-opt-cancel{width:30px;height:30px}
}
`;

  window.OfferSheet = OfferSheet;
})();
