// beneficiaries-screen.jsx
// Step between Analysis and Coverage Insights.
// Confirms each beneficiary's email, phone, and relationship to the policy
// owner. Drives a one-at-a-time chat-rail card. Main canvas shows a living
// Sankey of policies → people that fills in as each is confirmed.

const { useState: bsS, useMemo: bsM, useRef: bsR, useEffect: bsE } = React;

// Build a "people" list from policies (one row per unique name).
// Also returns the set of policies with NO beneficiary on file —
// those are surfaced as placeholder nodes in the Sankey and slot
// into the card-flow as "add a person" steps.
function bsBuildPeople(policies) {
  const m = new Map();
  const unassigned = [];
  policies.forEach((p) => {
    const beneList = p.beneficiariesDetail || [];
    if (beneList.length === 0) {
      unassigned.push({
        carrier: p.carrier,
        faceAmount: p.faceAmount,
        faceAmountNum: p.faceAmountNum || 0,
        policyNumber: p.policyNumber,
        coverageType: p.coverageType,
      });
      return;
    }
    const total = beneList.reduce((a, b) => a + (b.pct || 0), 0) || 100;
    beneList.forEach((b) => {
      const key = b.name;
      if (!m.has(key)) {
        m.set(key, {
          name: b.name,
          relationship: b.relationship || "",
          email: b.email || "",
          phone: b.phone || "",
          policies: [],
          confirmed: false,
          source: "extracted",
        });
      }
      const person = m.get(key);
      const dollarShare = (p.faceAmountNum || 0) * ((b.pct || 0) / total);
      person.policies.push({
        carrier: p.carrier,
        faceAmount: p.faceAmount,
        faceAmountNum: p.faceAmountNum || 0,
        policyNumber: p.policyNumber,
        pct: b.pct,
        dollarShare,
        coverageType: p.coverageType,
      });
    });
  });
  return { people: [...m.values()], unassignedPolicies: unassigned };
}

const RELATIONSHIPS = [
  "Spouse", "Partner",
  "Son", "Daughter", "Child",
  "Mother", "Father", "Parent",
  "Sister", "Brother", "Sibling",
  "Grandchild", "Niece", "Nephew",
  "Friend", "Trust", "Estate", "Other",
];

// Brand-palette destinations (same DNA as the downstream Sankey)
const BS_PALETTE = [
  { light: "#FFE4CB", base: "#FFB876", deep: "#D38D54" }, // peach/orange
  { light: "#CFEDF1", base: "#A1DCE2", deep: "#70B6BA" }, // sky blue
  { light: "#E5F0AE", base: "#C0C983", deep: "#8D9933" }, // soft chartreuse
  { light: "#E2D7F4", base: "#C6B3EB", deep: "#9888BF" }, // soft purple
  { light: "#FAE8B0", base: "#F2D26B", deep: "#C9A93C" }, // honey
  { light: "#F5C9C9", base: "#E89C9C", deep: "#B86A6A" }, // dusty rose
];

function bsInitials(name) {
  return name.split(" ").map(s => s[0]).join("").slice(0, 2).toUpperCase();
}

// Tiny carrier chip
const BSCarrier = ({ name }) => {
  const palette = {
    "Banner Life":   { bg: "#FFFFFF", fg: "#006EF5", border: "1px solid var(--helm-dark-ecru)" },
    "New York Life": { bg: "#0033A0", fg: "#FFFFFF" },
    "Pacific Life":  { bg: "#0F2A4F", fg: "#FFFFFF" },
    "MetLife":       { bg: "#0061A0", fg: "#FFFFFF" },
    "Prudential":    { bg: "#003DA5", fg: "#FFFFFF" },
  }[name] || { bg: "var(--helm-ecru)", fg: "var(--helm-dark-green)" };
  return (
    <span style={{
      display: "inline-flex", alignItems: "center", justifyContent: "center",
      height: 20, padding: "0 8px", borderRadius: 3,
      background: palette.bg, color: palette.fg,
      border: palette.border || "none",
      fontFamily: "var(--font-sans)", fontWeight: 600, fontSize: 10, letterSpacing: "-0.01em",
      whiteSpace: "nowrap",
    }}>{name}</span>
  );
};

// Tiny field used inside the chat-rail card.
// Placeholder-only design — the leading icon doubles as the label cue
// to keep the card compact for the right rail. `required` + `missing`
// shows a small "needed" pill on the right when validation has fired.
// Format a digit string as North-American "(###) ###-####". Accepts any
// input (we strip non-digits), preserves an optional leading "1" country
// code, and truncates at 10 significant digits. Partial input formats
// progressively so the parens/space/dash appear as the user types.
function formatNAPhone(raw) {
  let d = (raw || "").replace(/\D/g, "");
  if (d.length > 11) d = d.slice(0, 11);
  if (d.length === 11 && d.startsWith("1")) d = d.slice(1);
  if (d.length > 10) d = d.slice(0, 10);
  if (d.length === 0) return "";
  if (d.length < 4)   return `(${d}`;
  if (d.length < 7)   return `(${d.slice(0,3)}) ${d.slice(3)}`;
  return `(${d.slice(0,3)}) ${d.slice(3,6)}-${d.slice(6)}`;
}

// Lightweight email validator — pragmatic, not RFC-5322. Matches
// "local@domain.tld" with at least a 2-char TLD and no whitespace.
function isValidEmail(s) {
  if (!s) return false;
  return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(s.trim());
}

const BSField = ({ value, onChange, placeholder, type = "text", icon, required, missing, missingText = "needed", mask, validate, attempted }) => {
  const [touched, setTouched] = bsS(false);
  // If a validate fn is provided, derive missing/text from it (live, after
  // the user has touched the field or after a confirm attempt). Otherwise
  // fall back to the caller-provided `missing` flag.
  let showMissing = missing;
  let showText = missingText;
  if (typeof validate === "function") {
    const v = validate(value);
    // v can be: true (ok) | false (error, "needed") | { text } (error w/ label)
    const ok = v === true || v === undefined || v === null;
    if (!ok && (touched || attempted)) {
      showMissing = true;
      showText = (v && v.text) || "needed";
    } else if (ok) {
      showMissing = false;
    }
  }
  return (
  <div style={{
    display: "flex", alignItems: "center", gap: 8,
    padding: "8px 11px",
    background: "var(--helm-white)",
    border: `1px solid ${showMissing ? "var(--helm-dark-orange)" : "var(--helm-dark-ecru)"}`,
    borderRadius: 7,
    minWidth: 0,
  }}>
    {icon}
    <input
      type={type} value={value}
      onChange={e => onChange(mask === "phone" ? formatNAPhone(e.target.value) : e.target.value)}
      onBlur={() => setTouched(true)}
      inputMode={mask === "phone" ? "tel" : undefined}
      autoComplete={mask === "phone" ? "tel-national" : undefined}
      maxLength={mask === "phone" ? 14 : undefined}
      placeholder={placeholder}
      aria-label={placeholder}
      style={{
        flex: 1, minWidth: 0, border: "none", outline: "none",
        background: "transparent", color: "var(--helm-dark-green)",
        fontFamily: "var(--font-sans)", fontSize: 13,
      }}
    />
    {required && showMissing && (
      <span style={{
        fontSize: 9, fontWeight: 600, letterSpacing: "0.06em",
        color: "var(--helm-dark-orange)",
        textTransform: "uppercase", flexShrink: 0,
      }}>{showText}</span>
    )}
  </div>
  );
};

// Custom dropdown (consistent with Helm's printed-paper aesthetic).
// Placeholder-only to match BSField. The selected value (or the
// `placeholder` prop) sits inline with the chevron. The menu renders
// through a portal so it can escape parent `overflow: hidden` (the
// card wrapper clips children to keep its rounded corners clean).
const BSSelect = ({ value, onChange, options, placeholder = "Choose…", icon, required, missing }) => {
  const [open, setOpen] = bsS(false);
  const [menuPos, setMenuPos] = bsS(null);
  const ref = bsR(null);
  const btnRef = bsR(null);
  bsE(() => {
    const onDoc = (e) => {
      // Treat clicks inside the trigger OR the portaled menu as inside.
      if (ref.current && ref.current.contains(e.target)) return;
      const menu = document.getElementById("bs-select-portal");
      if (menu && menu.contains(e.target)) return;
      setOpen(false);
    };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);
  // Position the menu under the trigger every time it opens, AND track
  // scroll/resize so it stays attached if the chat scrolls behind it.
  bsE(() => {
    if (!open) { setMenuPos(null); return; }
    const place = () => {
      const r = btnRef.current?.getBoundingClientRect();
      if (!r) return;
      setMenuPos({ left: r.left, top: r.bottom + 4, width: r.width });
    };
    place();
    window.addEventListener("scroll", place, true);
    window.addEventListener("resize", place);
    return () => {
      window.removeEventListener("scroll", place, true);
      window.removeEventListener("resize", place);
    };
  }, [open]);
  return (
    <div style={{ position: "relative", minWidth: 0 }} ref={ref}>
      <button ref={btnRef} type="button" onClick={() => setOpen(!open)} style={{
        display: "flex", alignItems: "center", gap: 8, width: "100%",
        padding: "8px 11px",
        background: "var(--helm-white)",
        border: `1px solid ${missing ? "var(--helm-dark-orange)" : "var(--helm-dark-ecru)"}`,
        borderRadius: 7, cursor: "pointer",
        fontFamily: "var(--font-sans)", fontSize: 13,
        color: value ? "var(--helm-dark-green)" : "var(--helm-dark-grey)",
        textAlign: "left",
      }}>
        {icon}
        <span style={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
          {value || placeholder}
        </span>
        {required && missing && (
          <span style={{
            fontSize: 9, fontWeight: 600, letterSpacing: "0.06em",
            color: "var(--helm-dark-orange)",
            textTransform: "uppercase", flexShrink: 0,
          }}>needed</span>
        )}
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"
          strokeWidth="2" style={{ opacity: 0.55, transform: open ? "rotate(180deg)" : "none", flexShrink: 0 }}>
          <path d="M6 9l6 6 6-6" />
        </svg>
      </button>
      {open && menuPos && ReactDOM.createPortal(
        <div id="bs-select-portal" style={{
          position: "fixed",
          left: menuPos.left, top: menuPos.top, width: menuPos.width,
          background: "var(--helm-white)",
          border: "1px solid var(--helm-dark-ecru)",
          borderRadius: 8, boxShadow: "0 12px 32px -8px rgba(0,29,0,0.22)",
          maxHeight: 240, overflow: "auto", zIndex: 1000,
        }}>
          {options.map((opt) => (
            <button key={opt} type="button"
              onClick={() => { onChange(opt); setOpen(false); }}
              style={{
                display: "block", width: "100%", textAlign: "left",
                padding: "9px 12px", background: opt === value ? "var(--helm-ecru)" : "transparent",
                border: "none", cursor: "pointer",
                fontFamily: "var(--font-sans)", fontSize: 13,
                color: "var(--helm-dark-green)",
              }}
              onMouseEnter={(e) => e.currentTarget.style.background = "var(--helm-ecru)"}
              onMouseLeave={(e) => e.currentTarget.style.background = opt === value ? "var(--helm-ecru)" : "transparent"}
            >{opt}</button>
          ))}
        </div>,
        document.body
      )}
    </div>
  );
};

// ────────────────────────────────────────────────────────────────────
// LIVING SANKEY — policies (left, ecru) → people (right, palette)
// Ribbon thickness = dollar share. Click a person to focus.
// ────────────────────────────────────────────────────────────────────
const BSSankey = ({ policies, people, unassignedPolicies = [], currentName, currentPolicyNumber, onPersonClick }) => {
  if (people.length === 0 && unassignedPolicies.length === 0) return null;

  const W = 720;
  const H = Math.max(420, people.length * 80);
  const GAP = 8;
  const polCol = { x: 0, w: 180 };
  const peopleCol = { x: W - 220, w: 220 };

  // Order people by their first-seen-on-policy index, then by name (stable)
  const peopleOrder = people.map(p => p.name);
  const colorFor = (name) => BS_PALETTE[peopleOrder.indexOf(name) % BS_PALETTE.length];

  // Total $ across everything (for scaling)
  const total = people.reduce((acc, p) => acc + p.policies.reduce((a, pol) => a + pol.dollarShare, 0), 0) || 1;

  // For each policy, collect rows {beneficiary, amount}
  const rows = [];
  policies.forEach((pol) => {
    people.forEach((person) => {
      person.policies.forEach((pp) => {
        if (pp.carrier === pol.carrier && pp.faceAmount === pol.faceAmount) {
          rows.push({
            policyKey: `${pol.carrier}|${pol.faceAmount}|${pol.policyNumber}`,
            policyLabel: pol.carrier,
            policyAmount: pol.faceAmountNum || 0,
            beneficiary: person.name,
            amount: pp.dollarShare,
          });
        }
      });
    });
  });

  // Stack policies vertically by their total $
  const polTotals = new Map();
  policies.forEach((p) => {
    const key = `${p.carrier}|${p.faceAmount}|${p.policyNumber}`;
    polTotals.set(key, p.faceAmountNum || 0);
  });
  const peopleTotals = new Map();
  people.forEach((p) => {
    peopleTotals.set(p.name, p.policies.reduce((a, pp) => a + pp.dollarShare, 0));
  });
  // Unassigned policies → one placeholder node per policy on the right.
  // We key them as "?<policyNumber>" so they don't collide with people names.
  const unassignedKeys = unassignedPolicies.map(pol => `?${pol.policyNumber}`);
  unassignedPolicies.forEach((pol) => {
    peopleTotals.set(`?${pol.policyNumber}`, pol.faceAmountNum || 0);
  });
  // Total recomputed to include unassigned $
  const totalAll = [...peopleTotals.values()].reduce((a, b) => a + b, 0) || 1;

  const usableH = H - 60;
  const scale = (v) => (v / totalAll) * (usableH - GAP * Math.max(policies.length, people.length + unassignedPolicies.length));

  // Position policies stacked
  const stackPositions = (entries) => {
    const m = new Map();
    let y = 30;
    entries.forEach(([k, v]) => {
      const h = scale(v);
      m.set(k, { y, h, v });
      y += h + GAP;
    });
    return m;
  };
  const polPos = stackPositions([...polTotals.entries()]);
  const benePos = stackPositions([...peopleTotals.entries()]);

  // Build segments with running offsets per side
  const ordered = [...rows].sort((a, b) => {
    // by policy stack order, then by beneficiary stack order
    const pa = [...polTotals.keys()].indexOf(a.policyKey);
    const pb = [...polTotals.keys()].indexOf(b.policyKey);
    if (pa !== pb) return pa - pb;
    return [...peopleTotals.keys()].indexOf(a.beneficiary) - [...peopleTotals.keys()].indexOf(b.beneficiary);
  });

  const polOff = new Map();
  const beneOff = new Map();
  const segments = [];

  ordered.forEach((r, idx) => {
    const h = scale(r.amount);
    const sp = polPos.get(r.policyKey);
    const bp = benePos.get(r.beneficiary);

    const pO = polOff.get(r.policyKey) || 0;
    const bO = beneOff.get(r.beneficiary) || 0;

    const yA = sp.y + pO,    yB = yA + h;
    const ybA = bp.y + bO,   ybB = ybA + h;

    const c = colorFor(r.beneficiary);

    const xS = polCol.x + polCol.w;
    const xE = peopleCol.x;
    const cx1 = (xS + xE) / 2;

    const path = `M${xS},${yA} C${cx1},${yA} ${cx1},${ybA} ${xE},${ybA} L${xE},${ybB} C${cx1},${ybB} ${cx1},${yB} ${xS},${yB} Z`;

    segments.push({
      key: `s-${idx}`,
      path,
      colorBase: c.base,
      colorLight: c.light,
      gradId: `bsg-${idx}`,
      beneficiary: r.beneficiary,
    });

    polOff.set(r.policyKey, pO + h);
    beneOff.set(r.beneficiary, bO + h);
  });

  // Build unassigned ribbons (policy → "?" node, gradient fades to grey)
  unassignedPolicies.forEach((pol, idx) => {
    const polKey = `${pol.carrier}|${pol.faceAmount}|${pol.policyNumber}`;
    const beneKey = `?${pol.policyNumber}`;
    const sp = polPos.get(polKey);
    const bp = benePos.get(beneKey);
    if (!sp || !bp) return;
    const h = sp.h;
    const yA = sp.y + (polOff.get(polKey) || 0);
    const yB = yA + h;
    const ybA = bp.y;
    const ybB = ybA + h;
    const xS = polCol.x + polCol.w;
    const xE = peopleCol.x;
    const cx1 = (xS + xE) / 2;
    const path = `M${xS},${yA} C${cx1},${yA} ${cx1},${ybA} ${xE},${ybA} L${xE},${ybB} C${cx1},${ybB} ${cx1},${yB} ${xS},${yB} Z`;
    segments.push({
      key: `u-${idx}`,
      path,
      gradId: `bsu-${idx}`,
      beneficiary: beneKey,
      isUnassigned: true,
    });
    polOff.set(polKey, (polOff.get(polKey) || 0) + h);
  });

  const fmt = (v) => `$${Math.round(v / 1000).toLocaleString()}K`;

  return (
    <svg viewBox={`-12 -12 ${W + 24} ${H + 24}`} style={{ width: "100%", height: "auto", display: "block", overflow: "visible" }}>
      <defs>
        {segments.map(s => s.isUnassigned ? (
          <linearGradient key={s.gradId} id={s.gradId} x1="0" x2="1" y1="0" y2="0">
            <stop offset="0%" stopColor="#F0E9CB" stopOpacity="0.6" />
            <stop offset="100%" stopColor="#B8B6A8" stopOpacity="0.5" />
          </linearGradient>
        ) : (
          <linearGradient key={s.gradId} id={s.gradId} x1="0" x2="1" y1="0" y2="0">
            <stop offset="0%" stopColor="#F0E9CB" stopOpacity="0.92" />
            <stop offset="100%" stopColor={s.colorBase} stopOpacity={s.beneficiary === currentName ? 0.95 : 0.78} />
          </linearGradient>
        ))}
        <pattern id="bsHatch" patternUnits="userSpaceOnUse" width="6" height="6" patternTransform="rotate(45)">
          <rect width="6" height="6" fill="#F4F1E4" />
          <line x1="0" y="0" x2="0" y2="6" stroke="#C9C5B3" strokeWidth="1.5" />
        </pattern>
      </defs>

      {/* Column headers */}
      <text x={polCol.x + polCol.w / 2} y={6} textAnchor="middle"
        style={{ fontFamily: "var(--font-sans)", fontSize: 11, fontWeight: 600,
          letterSpacing: "0.08em", textTransform: "uppercase", fill: "var(--helm-dark-grey)" }}>
        Policies
      </text>
      <text x={peopleCol.x + peopleCol.w / 2} y={6} textAnchor="middle"
        style={{ fontFamily: "var(--font-sans)", fontSize: 11, fontWeight: 600,
          letterSpacing: "0.08em", textTransform: "uppercase", fill: "var(--helm-dark-grey)" }}>
        People
      </text>

      {/* Policy nodes */}
      {[...polTotals.entries()].map(([k, v]) => {
        const p = polPos.get(k);
        const carrier = k.split("|")[0];
        return (
          <g key={`pol-${k}`}>
            <rect x={polCol.x} y={p.y} width={polCol.w} height={p.h} fill="#F0E9CB" rx={4} />
            <text x={polCol.x + 14} y={p.y + 22}
              style={{ fontFamily: "var(--font-sans)", fontSize: 13, fontWeight: 600, fill: "var(--helm-dark-green)" }}>
              {carrier}
            </text>
            {p.h > 30 && (
              <text x={polCol.x + 14} y={p.y + 40}
                style={{ fontFamily: "var(--font-sans)", fontSize: 12, fontWeight: 500, fill: "var(--helm-dark-green)", opacity: 0.7 }}>
                {fmt(v)}
              </text>
            )}
          </g>
        );
      })}

      {/* Ribbons (gradient policies → people) */}
      <g>
        {segments.map(s => (
          <path key={s.key} d={s.path}
            fill={`url(#${s.gradId})`}
            opacity={
              s.isUnassigned
                ? (currentPolicyNumber && s.beneficiary === `?${currentPolicyNumber}` ? 1 : (currentName || currentPolicyNumber) ? 0.5 : 0.85)
                : (!currentName || s.beneficiary === currentName ? 1 : 0.45)
            }
            style={{ transition: "opacity .25s" }} />
        ))}
      </g>

      {/* People nodes — clickable */}
      {people.map((person) => {
        const bp = benePos.get(person.name);
        const c = colorFor(person.name);
        const isCurrent = person.name === currentName;
        const totalForPerson = peopleTotals.get(person.name);
        return (
          <g key={`pe-${person.name}`} onClick={() => onPersonClick && onPersonClick(person.name)}
            style={{ cursor: "pointer" }}>
            {/* bg rect */}
            <rect x={peopleCol.x} y={bp.y} width={peopleCol.w} height={bp.h}
              fill={c.base} fillOpacity={person.confirmed ? 0.95 : isCurrent ? 0.85 : 0.55}
              rx={4}
              stroke={isCurrent ? "var(--helm-dark-green)" : "transparent"}
              strokeWidth={isCurrent ? 2 : 0} />
            {/* check tick if confirmed */}
            {person.confirmed && (
              <g transform={`translate(${peopleCol.x + peopleCol.w - 22}, ${bp.y + 12})`}>
                <circle cx={6} cy={6} r={9} fill="var(--helm-dark-green)" />
                <path d="M2 6.4 L5 9 L10 3.5" fill="none" stroke="var(--helm-white)" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
              </g>
            )}
            <text x={peopleCol.x + 14} y={bp.y + 22}
              style={{ fontFamily: "var(--font-sans)", fontSize: 14, fontWeight: 600, fill: "var(--helm-dark-green)" }}>
              {person.name}
            </text>
            {bp.h > 38 && (
              <text x={peopleCol.x + 14} y={bp.y + 40}
                style={{ fontFamily: "var(--font-sans)", fontSize: 12, fontWeight: 500, fill: "var(--helm-dark-green)", opacity: 0.75 }}>
                {fmt(totalForPerson)} {person.policies.length > 1 ? `· on ${person.policies.length} policies` : ""}
              </text>
            )}
          </g>
        );
      })}

      {/* Unassigned policy placeholders ("?" nodes) */}
      {unassignedPolicies.map((pol) => {
        const beneKey = `?${pol.policyNumber}`;
        const bp = benePos.get(beneKey);
        if (!bp) return null;
        const isCurrent = currentPolicyNumber === pol.policyNumber;
        return (
          <g key={`un-${pol.policyNumber}`}
            onClick={() => onPersonClick && onPersonClick(pol.policyNumber)}
            style={{ cursor: "pointer" }}>
            <rect x={peopleCol.x} y={bp.y} width={peopleCol.w} height={bp.h}
              fill="url(#bsHatch)"
              rx={4}
              stroke={isCurrent ? "var(--helm-dark-orange)" : "var(--helm-dark-grey)"}
              strokeWidth={isCurrent ? 2 : 1}
              strokeDasharray={isCurrent ? "0" : "4 3"} />
            <text x={peopleCol.x + 14} y={bp.y + 22}
              style={{ fontFamily: "var(--font-sans)", fontSize: 14, fontWeight: 600, fill: "var(--helm-dark-green)" }}>
              <tspan style={{ fill: "var(--helm-dark-orange)", fontWeight: 700 }}>?</tspan>{" "}Add a beneficiary
            </text>
            {bp.h > 38 && (
              <text x={peopleCol.x + 14} y={bp.y + 40}
                style={{ fontFamily: "var(--font-sans)", fontSize: 12, fontWeight: 500, fill: "var(--helm-dark-green)", opacity: 0.75 }}>
                {pol.carrier} · {fmt(pol.faceAmountNum || 0)}
              </text>
            )}
          </g>
        );
      })}
    </svg>
  );
};

// Inline (chip-style) version of BSSelect for places like the card
// title bar where we want a tiny, pill-shaped trigger that opens the
// same portaled menu. The pill ALWAYS reads as an input field — when
// empty, a dashed orange border + "needed" pill makes the required
// state unmistakable; when set, it settles into a quiet filled chip.
const BSInlineSelect = ({ value, onChange, options, placeholder = "Choose…", missing }) => {
  const [open, setOpen] = bsS(false);
  const [menuPos, setMenuPos] = bsS(null);
  const ref = bsR(null);
  const btnRef = bsR(null);
  bsE(() => {
    const onDoc = (e) => {
      if (ref.current && ref.current.contains(e.target)) return;
      const menu = document.getElementById("bs-select-portal");
      if (menu && menu.contains(e.target)) return;
      setOpen(false);
    };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);
  bsE(() => {
    if (!open) { setMenuPos(null); return; }
    const place = () => {
      const r = btnRef.current?.getBoundingClientRect();
      if (!r) return;
      setMenuPos({ left: r.left, top: r.bottom + 4, width: Math.max(r.width, 180) });
    };
    place();
    window.addEventListener("scroll", place, true);
    window.addEventListener("resize", place);
    return () => {
      window.removeEventListener("scroll", place, true);
      window.removeEventListener("resize", place);
    };
  }, [open]);

  const isEmpty = !value;
  const showNeeded = missing && isEmpty;
  // Two visual states: prompt (empty/required) vs filled (settled).
  const pillStyle = isEmpty ? {
    background: "var(--helm-white)",
    border: `1.5px dashed ${showNeeded ? "var(--helm-dark-orange)" : "var(--helm-dark-green)"}`,
    color: showNeeded ? "var(--helm-dark-orange)" : "var(--helm-dark-green)",
  } : {
    background: "var(--helm-white)",
    border: "1px solid var(--helm-dark-ecru)",
    color: "var(--helm-dark-green)",
  };

  return (
    <span ref={ref} style={{ display: "inline-flex", alignItems: "center", minWidth: 0 }}>
      <button ref={btnRef} type="button" onClick={() => setOpen(!open)} style={{
        display: "inline-flex", alignItems: "center", gap: 5,
        padding: "3px 9px 3px 10px", borderRadius: 999,
        cursor: "pointer",
        fontFamily: "var(--font-sans)", fontSize: 12, fontWeight: 600,
        lineHeight: 1.4,
        ...pillStyle,
      }}>
        <span style={{ whiteSpace: "nowrap" }}>{value || placeholder}</span>
        {showNeeded && (
          <span style={{
            display: "inline-flex", alignItems: "center", justifyContent: "center",
            minWidth: 14, height: 14, padding: "0 4px", borderRadius: 999,
            background: "var(--helm-dark-orange)", color: "var(--helm-white)",
            fontSize: 9, fontWeight: 700, letterSpacing: "0.02em",
            textTransform: "uppercase",
          }}>!</span>
        )}
        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"
          strokeWidth="2.2" style={{ opacity: 0.75, transform: open ? "rotate(180deg)" : "none" }}>
          <path d="M6 9l6 6 6-6" />
        </svg>
      </button>
      {open && menuPos && ReactDOM.createPortal(
        <div id="bs-select-portal" style={{
          position: "fixed",
          left: menuPos.left, top: menuPos.top, width: menuPos.width,
          background: "var(--helm-white)",
          border: "1px solid var(--helm-dark-ecru)",
          borderRadius: 8, boxShadow: "0 12px 32px -8px rgba(0,29,0,0.22)",
          maxHeight: 240, overflow: "auto", zIndex: 1000,
        }}>
          {options.map((opt) => (
            <button key={opt} type="button"
              onClick={() => { onChange(opt); setOpen(false); }}
              style={{
                display: "block", width: "100%", textAlign: "left",
                padding: "9px 12px", background: opt === value ? "var(--helm-ecru)" : "transparent",
                border: "none", cursor: "pointer",
                fontFamily: "var(--font-sans)", fontSize: 13,
                color: "var(--helm-dark-green)",
              }}
              onMouseEnter={(e) => e.currentTarget.style.background = "var(--helm-ecru)"}
              onMouseLeave={(e) => e.currentTarget.style.background = opt === value ? "var(--helm-ecru)" : "transparent"}
            >{opt}</button>
          ))}
        </div>,
        document.body
      )}
    </span>
  );
};

// ────────────────────────────────────────────────────────────────────
// THE SCREEN — main canvas
// ────────────────────────────────────────────────────────────────────
const BeneficiariesScreen = ({
  policies, setPolicies, onAdvance, onBack, density, onStepClick,
  people, setPeople, unassignedPolicies, setUnassignedPolicies,
  currentIdx, setCurrentIdx, onReopenCard,
}) => {

  // Augmented "slots" = real people + one slot per unassigned policy
  const slots = [
    ...people.map(p => ({ kind: "person", person: p })),
    ...(unassignedPolicies || []).map(pol => ({ kind: "addNew", policy: pol })),
  ];
  const totalSlots = slots.length;
  const slot = slots[currentIdx];
  const cur = slot && slot.kind === "person" ? slot.person : null;
  const total = totalSlots;

  const updateCurrent = (patch) => {
    if (!cur) return;
    setPeople(prev => prev.map((p, i) => i === currentIdx ? { ...p, ...patch } : p));
  };

  const isPersonValid = (p) => isValidEmail(p?.email) && !!(p?.relationship && p.relationship.trim());
  const allConfirmed = people.length > 0 && people.every(p => p.confirmed) && (unassignedPolicies || []).length === 0;
  const allValid = allConfirmed;

  const confirmAndNext = () => {
    if (!cur) return;
    if (!isPersonValid(cur)) {
      updateCurrent({ _attempted: true });
      return;
    }
    setPeople(prev => prev.map((p, i) => i === currentIdx ? { ...p, confirmed: true } : p));
    if (currentIdx < totalSlots - 1) setCurrentIdx(currentIdx + 1);
  };

  // Add a brand-new beneficiary from an unassigned-policy slot.
  const addNewFromSlot = (data) => {
    if (!slot || slot.kind !== "addNew") return;
    const pol = slot.policy;
    const newPerson = {
      name: data.name,
      email: data.email || "",
      phone: data.phone || "",
      relationship: data.relationship,
      confirmed: !!(data.email && data.relationship),
      source: "manual",
      policies: [{
        carrier: pol.carrier,
        faceAmount: pol.faceAmount,
        faceAmountNum: pol.faceAmountNum || 0,
        policyNumber: pol.policyNumber,
        pct: 100,
        dollarShare: pol.faceAmountNum || 0,
        coverageType: pol.coverageType,
      }],
    };
    setPeople(prev => [...prev, newPerson]);
    setUnassignedPolicies(prev => prev.filter(p => p.policyNumber !== pol.policyNumber));
    // After insertion, advance to next slot if any (the slots array shifts but length is same)
    // We stay on currentIdx because the new person now occupies the previous "addNew" slot's earlier index, and that slot is now removed.
    // Simpler: jump to next unconfirmed slot, else stay on last.
    setTimeout(() => {
      // Recompute against new state-shape on next tick.
      // We'll just go to last slot if past end.
    }, 0);
  };

  const goPrev = () => setCurrentIdx(Math.max(0, currentIdx - 1));
  const goNext = () => setCurrentIdx(Math.min(totalSlots - 1, currentIdx + 1));
  const jumpTo = (key) => {
    // key can be a person name OR a policy number for unassigned slots
    const idx = slots.findIndex(s =>
      (s.kind === "person" && s.person.name === key) ||
      (s.kind === "addNew" && s.policy.policyNumber === key)
    );
    if (idx !== -1) {
      setCurrentIdx(idx);
      // Re-summon the chat-rail card so the user can edit this person —
      // it would otherwise stay hidden if everything was already confirmed.
      onReopenCard && onReopenCard();
    }
  };

  const pad = density === "compact" ? "32px 56px" : "40px 64px";
  const confirmedCount = people.filter(p => p.confirmed).length;
  const remaining = totalSlots - confirmedCount;

  if (people.length === 0 && (unassignedPolicies || []).length === 0) {
    return (
      <main style={{ flex: 1, padding: pad, background: "var(--helm-white)", overflow: "auto" }}>
        <div style={{ maxWidth: 600 }}>
          <p style={{ opacity: 0.7 }}>No beneficiaries to confirm. Add a policy first.</p>
        </div>
      </main>
    );
  }

  return (
    <main style={{ flex: 1, padding: pad, background: "var(--helm-white)", overflow: "auto" }}>
      <div style={{ maxWidth: 880 }}>
        <StepBreadcrumb
          steps={window.HELM_STEPS}
          current={2}
          onStepClick={onStepClick}
        />

        <div style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 24, margin: "32px 0 14px" }}>
          <h1 style={{ fontFamily: "var(--font-serif)", fontSize: 48, lineHeight: "52px",
            fontWeight: 600, letterSpacing: "-0.015em", margin: 0, whiteSpace: "nowrap" }}>
            The {people.length} people you're protecting.
          </h1>
        </div>

        <div style={{ display: "flex", alignItems: "baseline", gap: 16, margin: "0 0 28px",
          flexWrap: "wrap" }}>
          <p style={{ fontFamily: "var(--font-sans)", fontSize: 18, lineHeight: "28px",
            opacity: 0.8, margin: 0, flex: 1, minWidth: 320, maxWidth: 620 }}>
            A few people appear on more than one policy — confirm each once and Helm reuses their info everywhere.
          </p>
          <div style={{ display: "flex", alignItems: "baseline", gap: 8, flexShrink: 0 }}>
            <span style={{ fontSize: 11, fontWeight: 600, opacity: 0.55,
              textTransform: "uppercase", letterSpacing: "0.08em" }}>Confirmed</span>
            <span style={{ fontFamily: "var(--font-sans)", fontSize: 22,
              fontWeight: 600, letterSpacing: "-0.01em", color: "var(--helm-dark-green)" }}>
              {confirmedCount}<span style={{ opacity: 0.4 }}>/{people.length}</span>
            </span>
          </div>
        </div>

        <div style={{
          padding: 32, background: "var(--helm-white)",
          border: "1px solid var(--helm-dark-ecru)",
          borderRadius: 16, boxShadow: "var(--shadow-card)",
        }}>
          <BSSankey
            policies={policies}
            people={people}
            unassignedPolicies={unassignedPolicies || []}
            currentName={cur?.name}
            currentPolicyNumber={slot?.kind === "addNew" ? slot.policy.policyNumber : null}
            onPersonClick={jumpTo}
          />
        </div>

        <div style={{ marginTop: 28, display: "flex", gap: 12, alignItems: "center" }}>
          <Button variant="primary" onClick={onAdvance} disabled={!allValid}>
            {allValid ? "Continue to payout guidance →" : `${remaining} ${remaining === 1 ? "person" : "people"} still to confirm`}
          </Button>
          {onBack && (
            <Button variant="secondary" onClick={onBack}>← Back to review</Button>
          )}
        </div>
      </div>
    </main>
  );
};

// ────────────────────────────────────────────────────────────────────
// CHAT-RAIL CARD — replaces the chat content while on this screen.
// Returns { messages, replies, customNode } so chat-guide can render it.
// ────────────────────────────────────────────────────────────────────
const BeneficiaryChatCard = ({ slots, knownPeople, currentIdx, onPrev, onNext, onChange, onConfirm, onAssign, attempted }) => {
  const slot = slots[currentIdx];
  if (!slot) return null;
  const total = slots.length;
  const isFirst = currentIdx === 0;
  const isLast = currentIdx === total - 1;
  const palette = BS_PALETTE[currentIdx % BS_PALETTE.length];
  // Revisit mode: every slot is a confirmed person and nothing is left
  // to assign — meaning the user came back to review one specific
  // beneficiary, not to walk through the queue. The CTA should then
  // always read "Confirm" rather than "Next →".
  const isRevisit = slots.length > 0 && slots.every(s =>
    s.kind === "person" && s.person.confirmed);

  // Render header (counter + arrows) — shared between modes
  const header = (
    <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 6, gap: 8 }}>
      <button onClick={onPrev} disabled={isFirst} aria-label="Previous beneficiary"
        style={{ width: 26, height: 26, borderRadius: "50%", background: "var(--helm-white)",
          border: "1px solid var(--helm-dark-ecru)", cursor: isFirst ? "not-allowed" : "pointer",
          opacity: isFirst ? 0.35 : 1, display: "flex", alignItems: "center", justifyContent: "center",
          color: "var(--helm-dark-green)", flexShrink: 0 }}>
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
          <path d="M15 18l-6-6 6-6" />
        </svg>
      </button>
      <div style={{ flex: 1, textAlign: "center", display: "flex", alignItems: "center", justifyContent: "center", gap: 10 }}>
        <div style={{ fontFamily: "var(--font-sans)", fontSize: 10, fontWeight: 600,
          letterSpacing: "0.08em", textTransform: "uppercase", opacity: 0.6, whiteSpace: "nowrap" }}>
          {slot.kind === "addNew" ? `Missing on ${slot.policy.carrier}` : `${currentIdx + 1} of ${total}`}
        </div>
        <div style={{ display: "flex", gap: 3, alignItems: "center" }}>
          {slots.map((s, i) => {
            const done = s.kind === "person" ? s.person.confirmed : false;
            return (
              <div key={i} style={{
                width: i === currentIdx ? 16 : 5, height: 5, borderRadius: 999,
                background: done ? "var(--helm-chartreuse-2)"
                          : i === currentIdx ? "var(--helm-dark-green)"
                          : s.kind === "addNew" ? "var(--helm-dark-orange)"
                          : "var(--helm-dark-ecru)",
                opacity: s.kind === "addNew" && i !== currentIdx ? 0.5 : 1,
                transition: "width .2s",
              }} />
            );
          })}
        </div>
      </div>
      <button onClick={onNext} disabled={isLast} aria-label="Next beneficiary"
        style={{ width: 26, height: 26, borderRadius: "50%", background: "var(--helm-white)",
          border: "1px solid var(--helm-dark-ecru)", cursor: isLast ? "not-allowed" : "pointer",
          opacity: isLast ? 0.35 : 1, display: "flex", alignItems: "center", justifyContent: "center",
          color: "var(--helm-dark-green)", flexShrink: 0 }}>
        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2">
          <path d="M9 18l6-6-6-6" />
        </svg>
      </button>
    </div>
  );

  if (slot.kind === "addNew") {
    return <BSAddNewCard slot={slot} header={header} knownPeople={knownPeople || []} onAssign={onAssign} attempted={attempted} />;
  }

  const cur = slot.person;
  const relMissing = attempted && !cur.relationship.trim();
  // Email validator returns true (ok) or { text } (error label).
  const emailValidate = (v) => {
    if (!v || !v.trim()) return { text: "needed" };
    if (!isValidEmail(v)) return { text: "invalid" };
    return true;
  };

  return (
    <div>
      {header}

      {/* The card — compact, placeholder-only form */}
      <div style={{
        background: "var(--helm-white)",
        border: "1px solid var(--helm-dark-ecru)",
        borderRadius: 12,
        boxShadow: "var(--shadow-card)",
        overflow: "hidden",
      }}>
        {/* Person strip — avatar + name + inline relationship selector.
            Relationship lives in the header (it's an attribute of the
            person, not separate form data) which saves a field row. */}
        <div style={{
          padding: "8px 12px",
          background: palette.light,
          borderBottom: "1px solid var(--helm-dark-ecru)",
          display: "flex", alignItems: "center", gap: 10,
        }}>
          <div style={{
            width: 32, height: 32, borderRadius: "50%",
            background: palette.base,
            color: "var(--helm-dark-green)",
            display: "inline-flex", alignItems: "center", justifyContent: "center",
            fontFamily: "var(--font-sans)", fontWeight: 600, fontSize: 12,
            flexShrink: 0,
          }}>{bsInitials(cur.name)}</div>
          <div style={{ flex: 1, minWidth: 0, display: "flex", alignItems: "center",
            flexWrap: "wrap", columnGap: 8, rowGap: 2 }}>
            <div style={{ fontFamily: "var(--font-sans)", fontSize: 14, fontWeight: 600,
              color: "var(--helm-dark-green)", whiteSpace: "nowrap",
              overflow: "hidden", textOverflow: "ellipsis", lineHeight: 1.15,
              minWidth: 0, flexShrink: 1 }}>
              {cur.name}
            </div>
            <span style={{
              fontFamily: "var(--font-sans)", fontSize: 12, opacity: 0.4,
              lineHeight: 1.15, flexShrink: 0,
            }}>·</span>
            <BSInlineSelect
              value={cur.relationship}
              missing={relMissing}
              placeholder="Set relationship"
              options={RELATIONSHIPS}
              onChange={v => onChange({ relationship: v })}
            />
          </div>
          {cur.confirmed && (
            <span style={{
              display: "inline-flex", alignItems: "center", gap: 4,
              height: 20, padding: "0 8px", borderRadius: 999,
              background: "var(--helm-dark-green)", color: "var(--helm-white)",
              fontSize: 9, fontWeight: 600, letterSpacing: "0.04em",
              flexShrink: 0,
            }}>
              <svg width="8" height="8" viewBox="0 0 16 12" fill="currentColor">
                <path d="M5.6 11.6L0 6l1.2-1.2L5.6 9.2 14.8 0 16 1.2z" />
              </svg>
              Confirmed
            </span>
          )}
        </div>

        {/* Form — email + phone only; relationship lives in the strip */}
        <div style={{ padding: "10px 12px 12px",
          display: "flex", flexDirection: "column", gap: 7 }}>
          <BSField placeholder="Email address" type="email" required
            validate={emailValidate} attempted={attempted}
            value={cur.email} onChange={v => onChange({ email: v })}
            icon={
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
                strokeWidth="1.6" style={{ opacity: 0.55, flexShrink: 0 }}>
                <rect x="3" y="5" width="18" height="14" rx="2" />
                <path d="M3 7l9 6 9-6" />
              </svg>
            } />
          <BSField placeholder="Phone (optional)" type="tel" mask="phone"
            value={cur.phone} onChange={v => onChange({ phone: v })}
            icon={
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
                strokeWidth="1.6" style={{ opacity: 0.55, flexShrink: 0 }}>
                <path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07 19.5 19.5 0 01-6-6 19.79 19.79 0 01-3.07-8.67A2 2 0 014.11 2h3a2 2 0 012 1.72c.13.96.37 1.9.72 2.81a2 2 0 01-.45 2.11L8.09 9.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45c.91.35 1.85.59 2.81.72A2 2 0 0122 16.92z" />
              </svg>
            } />

          <Button variant="primary" onClick={onConfirm}
            disabled={cur.confirmed && isLast && !isRevisit}
            style={{ marginTop: 4, width: "100%", padding: "9px 14px", fontSize: 13 }}>
            {isRevisit
              ? "Confirm"
              : cur.confirmed
                ? (isLast ? "Confirmed" : "Next →")
                : isLast ? "Confirm" : "Confirm & next →"}
          </Button>
          {cur.policies.length > 1 && (
            <div style={{ fontSize: 10, opacity: 0.55, fontStyle: "italic", textAlign: "center" }}>
              Applies to all {cur.policies.length} of {cur.name.split(" ")[0]}'s policies.
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

window.BeneficiariesScreen = BeneficiariesScreen;
window.BeneficiaryChatCard = BeneficiaryChatCard;
window.bsBuildPeople = bsBuildPeople;
window.isValidEmail = isValidEmail;

// ────────────────────────────────────────────────────────────────────
// BSAddNewCard — resolves a policy that arrived without a beneficiary.
// User can split it across multiple beneficiaries: each can be picked
// from the already-known people on this account (merge) or added new.
// ────────────────────────────────────────────────────────────────────
const BSAddNewCard = ({ slot, header, knownPeople, onAssign, attempted }) => {
  const policyFace = slot.policy.faceAmountNum || 0;
  const policyKey = slot.policy.policyNumber;

  // Each row: { id, kind: "existing"|"new", name, pct, email, relationship, phone, knownIdx? }
  const defaultKind = "existing";
  const initialKind = knownPeople.length > 0 ? "existing" : "new";
  const [rows, setRows] = bsS([
    { id: 1, kind: initialKind, knownName: knownPeople[0]?.name || "", name: "", pct: 100, email: "", relationship: "", phone: "" },
  ]);
  bsE(() => {
    // Reset whenever the policy changes
    const k = knownPeople.length > 0 ? "existing" : "new";
    setRows([{ id: 1, kind: k, knownName: knownPeople[0]?.name || "", name: "", pct: 100, email: "", relationship: "", phone: "" }]);
  }, [policyKey]);

  const totalPct = rows.reduce((a, r) => a + (Number(r.pct) || 0), 0);
  const pctOk = Math.round(totalPct) === 100;

  const updateRow = (id, patch) => setRows(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r));
  const addRow = () => {
    const remaining = Math.max(0, 100 - totalPct);
    const id = Math.max(0, ...rows.map(r => r.id)) + 1;
    const taken = new Set(rows.filter(r => r.kind === "existing" && r.knownName).map(r => r.knownName));
    const firstAvailable = knownPeople.map(p => p.name).find(n => !taken.has(n)) || "";
    // If everyone known is already picked, default this new row to "new person" instead.
    const nextKind = firstAvailable ? "existing" : "new";
    setRows(prev => [...prev, {
      id, kind: nextKind,
      knownName: firstAvailable, name: "", pct: remaining || 50,
      email: "", relationship: "", phone: "",
    }]);
  };
  const removeRow = (id) => setRows(prev => prev.length > 1 ? prev.filter(r => r.id !== id) : prev);
  const splitEvenly = () => {
    const each = Math.floor(100 / rows.length);
    const remainder = 100 - each * rows.length;
    setRows(prev => prev.map((r, i) => ({ ...r, pct: each + (i === 0 ? remainder : 0) })));
  };

  const submit = () => {
    if (!pctOk) return;
    const allocations = rows.map(r => {
      if (r.kind === "existing") {
        return { kind: "existing", name: r.knownName, pct: Number(r.pct) || 0 };
      }
      return {
        kind: "new", name: r.name.trim(), pct: Number(r.pct) || 0,
        email: r.email.trim(), relationship: r.relationship, phone: r.phone.trim(),
      };
    });
    // Validate
    const bad = allocations.find(a =>
      !a.name || (a.kind === "new" && !a.relationship)
    );
    if (bad) return; // attempted will surface inline misses below
    onAssign && onAssign({ mode: "mixed", policyNumber: policyKey, allocations });
  };

  const fmt = (n) => `$${Math.round(n).toLocaleString()}`;

  return (
    <div>
      {header}
      <div style={{
        background: "var(--helm-white)",
        border: "1px solid var(--helm-dark-orange)",
        borderRadius: 14,
        boxShadow: "var(--shadow-card)",
        overflow: "hidden",
      }}>
        <div style={{
          padding: "14px 18px",
          background: "#FFF1E1",
          borderBottom: "1px solid var(--helm-dark-ecru)",
          display: "flex", alignItems: "center", gap: 12,
        }}>
          <div style={{
            width: 40, height: 40, borderRadius: "50%",
            background: "var(--helm-white)",
            border: "1.5px dashed var(--helm-dark-orange)",
            color: "var(--helm-dark-orange)",
            display: "inline-flex", alignItems: "center", justifyContent: "center",
            fontFamily: "var(--font-sans)", fontWeight: 700, fontSize: 18,
            flexShrink: 0,
          }}>?</div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontFamily: "var(--font-sans)", fontSize: 16, fontWeight: 700,
              color: "var(--helm-dark-green)", letterSpacing: "-0.005em" }}>
              {slot.policy.carrier} · {slot.policy.faceAmount}
            </div>
            <div style={{ fontSize: 11, opacity: 0.7, marginTop: 2,
              color: "var(--helm-dark-orange)", fontWeight: 600,
              textTransform: "uppercase", letterSpacing: "0.06em" }}>
              No beneficiary on file
            </div>
          </div>
        </div>

        <div style={{ padding: "10px 18px", display: "flex", flexDirection: "column", gap: 10 }}>
          <div style={{ fontSize: 12, opacity: 0.75, lineHeight: 1.45 }}>
            Who should receive this policy? Pick someone you've already added or enter a new person.
          </div>

          {rows.map((r, i) => {
            const dollar = policyFace * ((Number(r.pct) || 0) / 100);
            const showRemove = rows.length > 1;
            const takenInOtherRows = new Set(
              rows.filter(o => o.id !== r.id && o.kind === "existing" && o.knownName).map(o => o.knownName)
            );
            const anyAvailableForThisRow = knownPeople.some(p => !takenInOtherRows.has(p.name));
            const existingDisabled = knownPeople.length === 0 || !anyAvailableForThisRow;
            return (
              <div key={r.id} style={{
                border: "1px solid var(--helm-dark-ecru)", borderRadius: 10,
                padding: 12, display: "flex", flexDirection: "column", gap: 8,
                background: "var(--helm-ecru)",
              }}>
                {/* Header row: kind toggle + remove */}
                <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
                  <div style={{ display: "flex", borderRadius: 999, overflow: "hidden",
                    border: "1px solid var(--helm-dark-ecru)", background: "var(--helm-white)", flex: 1 }}>
                    <button onClick={() => updateRow(r.id, { kind: "existing" })}
                      disabled={existingDisabled}
                      style={{
                        flex: 1, padding: "5px 8px", fontSize: 11, fontWeight: 600,
                        background: r.kind === "existing" ? "var(--helm-dark-green)" : "transparent",
                        color: r.kind === "existing" ? "var(--helm-white)" : "var(--helm-dark-green)",
                        border: "none", cursor: existingDisabled ? "not-allowed" : "pointer",
                        opacity: existingDisabled ? 0.4 : 1,
                      }}>Pick someone</button>
                    <button onClick={() => updateRow(r.id, { kind: "new" })}
                      style={{
                        flex: 1, padding: "5px 8px", fontSize: 11, fontWeight: 600,
                        background: r.kind === "new" ? "var(--helm-dark-green)" : "transparent",
                        color: r.kind === "new" ? "var(--helm-white)" : "var(--helm-dark-green)",
                        border: "none", cursor: "pointer",
                      }}>New person</button>
                  </div>
                  {showRemove && (
                    <button onClick={() => removeRow(r.id)} aria-label="Remove"
                      style={{
                        width: 24, height: 24, borderRadius: "50%",
                        background: "var(--helm-white)",
                        border: "1px solid var(--helm-dark-ecru)",
                        cursor: "pointer", color: "var(--helm-dark-green)",
                        display: "inline-flex", alignItems: "center", justifyContent: "center",
                      }}>
                      <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
                        <path d="M6 6l12 12M18 6L6 18" />
                      </svg>
                    </button>
                  )}
                </div>

                {/* Identity */}
                {r.kind === "existing" ? (() => {
                  // Exclude beneficiaries already chosen in OTHER rows of this card.
                  const takenInOtherRows = new Set(
                    rows
                      .filter(other => other.id !== r.id && other.kind === "existing" && other.knownName)
                      .map(other => other.knownName)
                  );
                  const availableNames = knownPeople
                    .map(p => p.name)
                    .filter(n => !takenInOtherRows.has(n));
                  // Keep this row's current choice in the list even if (somehow) duplicated,
                  // so the displayed value never silently disappears.
                  if (r.knownName && !availableNames.includes(r.knownName)) {
                    availableNames.unshift(r.knownName);
                  }
                  return (
                    <BSSelect placeholder="Choose beneficiary" required value={r.knownName}
                      missing={attempted && !r.knownName}
                      onChange={v => updateRow(r.id, { knownName: v })}
                      options={availableNames} />
                  );
                })() : (
                  <>
                    <BSField placeholder="Full name (e.g. Jordan Garcia)" required
                      missing={attempted && !r.name.trim()}
                      value={r.name} onChange={v => updateRow(r.id, { name: v })} />
                    <BSSelect placeholder="Relationship to you" required
                      missing={attempted && !r.relationship}
                      value={r.relationship} onChange={v => updateRow(r.id, { relationship: v })}
                      options={RELATIONSHIPS} />
                  </>
                )}

                {/* Allocation */}
                <div style={{ display: "flex", alignItems: "flex-end", gap: 8 }}>
                  <div style={{ flex: 1 }}>
                    <div style={{
                      fontFamily: "var(--font-sans)", fontSize: 10, fontWeight: 600,
                      letterSpacing: "0.08em", textTransform: "uppercase", opacity: 0.55,
                      marginBottom: 4,
                    }}>Share</div>
                    <div style={{ display: "flex", alignItems: "center", gap: 6,
                      background: "var(--helm-white)", border: "1px solid var(--helm-dark-ecru)",
                      borderRadius: 6, padding: "6px 10px" }}>
                      <input type="number" min="0" max="100" value={r.pct}
                        onChange={e => updateRow(r.id, { pct: Math.max(0, Math.min(100, Number(e.target.value) || 0)) })}
                        style={{
                          width: 50, border: "none", outline: "none", background: "transparent",
                          fontFamily: "var(--font-sans)", fontSize: 14, fontWeight: 600,
                          color: "var(--helm-dark-green)", textAlign: "right",
                        }} />
                      <span style={{ fontSize: 12, fontWeight: 600, opacity: 0.6 }}>%</span>
                    </div>
                  </div>
                  <div style={{ fontSize: 11, opacity: 0.7, paddingBottom: 8, whiteSpace: "nowrap" }}>
                    ≈ {fmt(dollar)}
                  </div>
                </div>
              </div>
            );
          })}

          {/* Tools row */}
          <div style={{ display: "flex", alignItems: "center", gap: 10, justifyContent: "space-between" }}>
            <button onClick={addRow}
              style={{
                background: "transparent", border: "1px dashed var(--helm-dark-ecru)",
                borderRadius: 999, padding: "5px 10px", fontSize: 11, fontWeight: 600,
                color: "var(--helm-dark-green)", cursor: "pointer",
                display: "inline-flex", alignItems: "center", gap: 4,
              }}>+ Add another</button>
            {rows.length > 1 && (
              <button onClick={splitEvenly}
                style={{
                  background: "transparent", border: "none",
                  fontSize: 11, fontWeight: 600, color: "var(--helm-dark-green)",
                  cursor: "pointer", textDecoration: "underline", opacity: 0.75,
                }}>Split evenly</button>
            )}
          </div>

          {/* Total */}
          <div style={{
            display: "flex", alignItems: "center", justifyContent: "space-between",
            padding: "8px 10px", borderRadius: 6,
            background: pctOk ? "rgba(166, 198, 84, 0.18)" : "rgba(217, 119, 87, 0.12)",
            border: `1px solid ${pctOk ? "var(--helm-chartreuse-2)" : "var(--helm-dark-orange)"}`,
          }}>
            <span style={{ fontSize: 11, fontWeight: 600, opacity: 0.75 }}>Total allocation</span>
            <span style={{ fontFamily: "var(--font-sans)", fontSize: 14, fontWeight: 700,
              color: pctOk ? "var(--helm-dark-green)" : "var(--helm-dark-orange)" }}>
              {totalPct}% {pctOk ? "✓" : `· must equal 100%`}
            </span>
          </div>
        </div>

        <div style={{ padding: "10px 18px 18px" }}>
          <Button variant="primary" onClick={submit}
            disabled={!pctOk}
            style={{ width: "100%" }}>
            Save beneficiaries →
          </Button>
        </div>
      </div>
    </div>
  );
};

window.BSAddNewCard = BSAddNewCard;
