// The Boardroom — custom boards UI (CUSTOM-BOARDS-UI-V1, design handoff
// `docs/design/The_Boardroom_Custom_Boards.zip`). Four surfaces:
//   AllMembersScreen  — the roster (the app's home when no board is active)
//   NameStepScreen    — 1f, the name step that COMPLETES creation
//   BoardSwitcher     — 1e + 2d, the top-bar board switcher
//   BoardManageScreen — 1g, rename / remove / add / delete
//
// Rulings folded in (Mike, 2026-07-28):
//  1. Consult affordance = 2b (stated actions) MODIFIED for density: 32px portraits
//     and md (38px) buttons whose tap area is padded invisibly to 44px. The full
//     descriptor is untouched — never truncated, never shortened.
//  3. Descriptors are the app's OWN `note` from app_experts, never the prototype's
//     two-word notes. Member ids were lifted; descriptors were not.
// The seat-grid metaphor (1b/1c/1d) is dead — 1a evolved into this roster.
const BoardsUI = (function () {
  const DS = window.TheBoardroomDesignSystem_4eda1b;

  const boardsCSS = `
/* Sticky discipline headers — the roster is read group by group. */
.brm-group { position:sticky; top:0; z-index:2; background:var(--paper); padding:20px 16px 9px; }
.brm-head { padding:24px 16px 4px; }
.brm-title { font-family:var(--font-serif); font-size:26px; letter-spacing:-0.015em; margin:0 0 6px; color:var(--ink-900); }
.brm-lede { font-family:var(--font-serif); font-size:15px; line-height:1.55; color:var(--ink-700); margin:0; }

/* The row. 32px portrait, name, FULL descriptor, then the two stated actions. */
.brm-row { display:flex; gap:12px; padding:14px 16px; border-bottom:1px solid var(--line-faint);
           border-left:2px solid transparent; background:transparent;
           transition:background var(--dur-fast) var(--ease-standard), border-color var(--dur-fast) var(--ease-standard); }
.brm-row--seated { border-left-color:var(--claret); background:var(--claret-wash); }
/* The cap: at 8 seated the unselected go quiet. Nothing is said. */
.brm-row--capped { opacity:.35; pointer-events:none; }
.brm-name { font-family:var(--font-serif); font-size:17px; line-height:1.25; color:var(--ink-900); }
.brm-desc { font-family:var(--font-serif); font-size:13.5px; line-height:1.5; color:var(--ink-700); margin-top:2px; }
.brm-actions { display:flex; gap:8px; margin-top:9px; flex-wrap:wrap; }
/* Invisible tap extension: md buttons read at 38px but answer to a 44px finger. */
.brm-tap44 { position:relative; }
.brm-tap44::after { content:""; position:absolute; left:0; right:0; top:50%;
                    transform:translateY(-50%); height:44px; }
/* The app-wide phone rule grows every .ds-btn to 44px tall. On the roster that would
   undo the density Mike asked for (ruling 1: md buttons, 32px portraits), so these two
   keep their md height and buy the 44px target back through ::after above. */
@media (max-width:720px) { .brm-tap44 { min-height:38px !important; } }

/* The footer appears from one seated. */
.brm-foot { position:sticky; bottom:0; z-index:3; display:flex; align-items:center; justify-content:space-between;
            gap:14px; padding:12px 16px 16px; border-top:1px solid var(--line);
            background:rgba(246,244,239,.92); backdrop-filter:blur(6px); }
.brm-count { font-family:var(--font-mono); font-size:10.5px; letter-spacing:.16em;
             text-transform:uppercase; color:var(--ink-500); }

/* Switcher — the dark bar's board name and its menu panel. */
.brm-swbtn { display:flex; align-items:center; gap:8px; min-height:44px; border:0; background:transparent;
             cursor:pointer; padding:0; font-family:var(--font-serif); font-size:17px; color:var(--paper); }
.brm-scrim { position:fixed; inset:0; background:rgba(27,26,23,.42); z-index:60; border:0; padding:0; }
.brm-panel { position:fixed; top:calc(10px + env(safe-area-inset-top, 0px)); left:12px; width:250px; z-index:61;
             background:var(--paper-raised); border:1px solid var(--line); border-radius:var(--radius-2);
             box-shadow:var(--shadow-2); overflow:hidden;
             animation:brm-in var(--dur-fast) var(--ease-standard); }
@keyframes brm-in { from { opacity:0; transform:translateY(-4px); } to { opacity:1; transform:none; } }
.brm-panel-h { font-family:var(--font-mono); font-size:10.5px; letter-spacing:.16em; text-transform:uppercase;
               color:var(--ink-500); padding:13px 14px 8px; }
.brm-list { max-height:352px; overflow-y:auto; }
.brm-brow { display:flex; align-items:center; justify-content:space-between; gap:10px; width:100%;
            padding:11px 14px 11px 12px; border:0; border-left:2px solid transparent; background:transparent;
            cursor:pointer; text-align:left; }
.brm-brow--on { border-left-color:var(--claret); background:var(--claret-wash); }
.brm-bname { font-family:var(--font-serif); font-size:15.5px; color:var(--ink-900); }
.brm-brow--on .brm-bname { color:var(--claret-deep); }
.brm-bcount { font-family:var(--font-mono); font-size:9.5px; color:var(--ink-500); flex:none; }
.brm-new { display:flex; align-items:center; gap:8px; width:100%; padding:11px 14px 13px; border:0;
           background:transparent; cursor:pointer; text-align:left;
           font-family:var(--font-sans); font-size:13.5px; color:var(--claret); }
.brm-full { padding:12px 14px 13px; border-bottom:1px solid var(--line-faint); }

/* Management — the member rows carry the same language at list density. */
.brm-mrow { display:flex; align-items:center; gap:12px; padding:11px 0; border-bottom:1px solid var(--line-faint); }
.brm-danger { display:flex; align-items:center; gap:8px; width:100%; padding:14px 0 0; border:0; background:transparent;
              cursor:pointer; text-align:left; font-family:var(--font-sans); font-size:13.5px; color:var(--critical); }

/* Share (4a–4c). A property of THIS board, sat between Add members and Delete,
   fenced from both by a rule. Nothing here says private, secret, secure,
   temporary or expiring — the link is none of those. */
.brm-share { border-top:1px solid var(--line); margin-top:26px; padding-top:16px; }
.brm-share-line { font-family:var(--font-serif); font-size:14.5px; line-height:1.55; color:var(--ink-700); margin:10px 0 12px; }
.brm-share-card { background:var(--paper-raised); border:1px solid var(--line-strong); border-radius:var(--radius-2);
                  padding:13px 14px; }
.brm-share-card--fresh { border-color:var(--claret-line); }
.brm-stem { display:flex; align-items:center; gap:8px; min-width:0; }
/* One opaque line. Never wrapped, never an input, never editable — the URL is a
   thing you copy, not a thing you edit. */
.brm-url { font-family:var(--font-mono); font-size:12px; color:var(--ink-900);
           white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; flex:1; }
.brm-url-box { background:var(--paper-sunken); border:1px solid var(--line); border-radius:var(--radius-2);
               padding:11px 12px; margin:14px 0 4px; }
.brm-share-meta { font-family:var(--font-mono); font-size:10.5px; letter-spacing:.14em; text-transform:uppercase;
                  color:var(--ink-500); margin-top:9px; }
/* Routine actions left, destructive pushed to the far edge — never adjacent. */
.brm-share-actions { display:flex; align-items:center; gap:14px; margin-top:12px; }
.brm-share-spacer { flex:1; }
.brm-share-note { font-family:var(--font-serif); font-size:13.5px; line-height:1.5; color:var(--ink-700); margin-top:10px; }

/* 5a — the list. A link is a PERSON, so the row leads with the name. */
.brm-lrow { display:flex; align-items:center; gap:12px; padding:14px 0;
            border-bottom:1px solid var(--line-faint); }
.brm-lname { font-family:var(--font-serif); font-size:17px; line-height:1.25; color:var(--ink-900); }
/* Path only, masked, one line — a row never re-exposes any of the token. */
.brm-lstem { font-family:var(--font-mono); font-size:11.5px; color:var(--ink-500); margin-top:3px;
             white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* The cap reads as a FACT in the same ink at 40/40 — never --caution, never a banner. */
.brm-lmeta { font-family:var(--font-mono); font-size:10.5px; letter-spacing:.1em; color:var(--ink-500); margin-top:4px; }
.brm-lacts { display:flex; align-items:center; gap:8px; flex:none; }

/* 5c — the row's action sheet. Bottom sheet, rounded at the top only. */
.brm-sheet { position:fixed; left:0; right:0; bottom:0; z-index:61; background:var(--paper-raised);
             border-radius:12px 12px 0 0; box-shadow:var(--shadow-3);
             padding:18px 16px calc(14px + env(safe-area-inset-bottom, 0px));
             animation:brm-up var(--dur) var(--ease-standard); }
@keyframes brm-up { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } }
.brm-sheet-h { font-family:var(--font-serif); font-size:19px; color:var(--ink-900); }
.brm-sheet-row { display:block; width:100%; text-align:left; border:0; background:transparent; cursor:pointer;
                 padding:13px 0; border-top:1px solid var(--line-faint); }
.brm-sheet-label { display:block; font-family:var(--font-sans); font-size:14.5px; color:var(--ink-900); }
.brm-sheet-row--critical .brm-sheet-label { color:var(--critical); }
/* The sentence does the teaching, at the point of use — which is why rotate and
   replace need no confirmation dialog. */
.brm-sheet-line { display:block; font-family:var(--font-serif); font-size:13.5px; line-height:1.5;
                  color:var(--ink-700); margin-top:3px; }
/* A sheet row that PUSHES a screen rather than acting: the chevron is the promise. */
.brm-sheet-row--door { display:flex; align-items:flex-start; gap:10px; }
.brm-sheet-door-x { flex:none; margin-top:2px; color:var(--ink-300); }

/* 6b — the background screen. Note is a field; the document is a ROW, because a
   30,000-character body does not belong in a box you scroll inside another box. */
.brm-bg { max-width:720px; margin:0 auto; padding:18px 20px 120px; width:100%; box-sizing:border-box; }
.brm-bg-line { font-family:var(--font-serif); font-size:13.5px; line-height:1.5; color:var(--ink-700); margin:8px 0 10px; }
.brm-doc-card { display:flex; align-items:center; gap:12px; width:100%; text-align:left; cursor:pointer;
                background:var(--paper-raised); border:1px solid var(--line-strong);
                border-radius:var(--radius-2); padding:14px; }
.brm-doc-empty { background:var(--paper-sunken); border:1px dashed var(--line-strong); }
.brm-doc-words { font-family:var(--font-mono); font-size:10.5px; letter-spacing:.1em; color:var(--ink-500); }
/* Two-line clamp of the opening — enough to tell WHICH document this is. */
.brm-doc-open { font-family:var(--font-serif); font-size:13.5px; line-height:1.45; color:var(--ink-900);
                margin-top:3px; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
                overflow:hidden; }
.brm-doc-empty-l { font-family:var(--font-serif); font-size:15px; color:var(--ink-900); }
.brm-doc-cap { font-family:var(--font-mono); font-size:10.5px; letter-spacing:.1em; color:var(--ink-500); margin-top:6px; }
.brm-bg-foot { position:fixed; left:0; right:0; bottom:0; z-index:5; display:flex; align-items:center;
               justify-content:space-between; gap:14px; padding:12px 20px calc(16px + env(safe-area-inset-bottom,0px));
               border-top:1px solid var(--line); background:rgba(246,244,239,.92); backdrop-filter:blur(6px); }

/* 6c — the document screen. The WHOLE VIEWPORT is the text: no card, no chrome,
   because pasting and re-reading is the only thing this screen does. */
.brm-docwrap { padding:18px 16px 120px; }
.brm-doctext { width:100%; border:0; outline:none; resize:none; background:transparent; min-height:60vh;
               font-family:var(--font-serif); font-size:15.5px; line-height:1.62; color:var(--ink-900); }
.brm-docbar { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:14px 16px 0; }
.brm-docbar-n { font-family:var(--font-mono); font-size:10.5px; letter-spacing:.14em; text-transform:uppercase;
                color:var(--claret-soft); }
.brm-doc-clear { background:transparent; border:0; padding:0; cursor:pointer; font-family:var(--font-sans);
                 font-size:13px; color:var(--critical); }
`;

  // Zero-padded mono index, the roster's counting idiom ("06").
  const pad2 = (n) => (n < 10 ? "0" : "") + n;

  // ── The roster — the app's home when no board is active ────────────────────
  // `mode`: "create" (seating builds a new board) or "add" (pre-seeded from a
  // board being managed). Only the footer label differs; the surface is one.
  function AllMembersScreen({ experts, seated, cap, onToggle, onConsult, onSeatBoard, onBack, mode }) {
    const { Button, Avatar, Kicker } = DS;
    const groups = window.BR.groupRoster(experts);
    const max = cap || 8;
    const atCap = seated.size >= max;
    const n = seated.size;
    return (
      <React.Fragment>
        <style>{boardsCSS}</style>
        {/* Clear the sticky footer: without this the LAST member sits under it
            permanently, unreachable by scrolling. Reserved only when it is shown. */}
        <div style={{ paddingBottom: n > 0 ? 104 : 8 }}>
          <div className="brm-head">
            {onBack && (
              <Button variant="ghost" size="sm" onClick={onBack}>
                <Icon name="chevronLeft" size={15} stroke="currentColor" /> Back
              </Button>
            )}
            <h1 className="brm-title">All members</h1>
            <p className="brm-lede">Consult a member one to one, or seat them onto a board of your own.</p>
          </div>
          {groups.map((g, gi) => (
            <div key={g.key}>
              <div className="brm-group"><Kicker index={pad2(gi + 1)}>{g.label}</Kicker></div>
              {g.members.map((m) => {
                const on = seated.has(m.id);
                const quiet = atCap && !on;
                return (
                  <div key={m.id} className={"brm-row" + (on ? " brm-row--seated" : "") + (quiet ? " brm-row--capped" : "")}>
                    <Avatar name={m.name} size="sm" />
                    <div style={{ minWidth: 0, flex: 1 }}>
                      <div className="brm-name">{m.name}</div>
                      {/* The app's own descriptor, in full — never truncated, never a short label. */}
                      <div className="brm-desc">{m.note}</div>
                      <div className="brm-actions">
                        <Button size="md" className="brm-tap44" variant={on ? "primary" : "secondary"}
                          aria-pressed={on} onClick={() => onToggle(m.id)}>
                          {on ? "Seated" : "Seat"}
                        </Button>
                        {onConsult && (
                          <Button size="md" className="brm-tap44" variant="ghost" onClick={() => onConsult(m)}>
                            Consult
                          </Button>
                        )}
                      </div>
                    </div>
                  </div>
                );
              })}
            </div>
          ))}
        </div>
        {/* Absent at zero seated — the footer is the way out, and there is nothing to leave with yet. */}
        {n > 0 && (
          <div className="brm-foot">
            <span className="brm-count">{n} of {max} seated</span>
            <Button size="lg" onClick={onSeatBoard}>{mode === "add" ? "Add members" : "Seat the board"}</Button>
          </div>
        )}
      </React.Fragment>
    );
  }

  // ── 1f — the name step. In the accepted flow members are already seated, so
  // this step COMPLETES creation (the bundle's "Choose members" button belonged
  // to the abandoned name-first order and would announce a step that never runs).
  function NameStepScreen({ count, cap, busy, error, onBack, onSave }) {
    const { Button, Input, Kicker } = DS;
    const MAX = 48;
    const [name, setName] = React.useState("");
    const trimmed = name.trim();
    const left = MAX - name.length;
    return (
      <React.Fragment>
        <style>{boardsCSS}</style>
        <div style={{ padding: "14px 16px 0" }}>
          <Button variant="ghost" size="sm" onClick={onBack}>
            <Icon name="chevronLeft" size={15} stroke="currentColor" /> New board
          </Button>
        </div>
        <div style={{ padding: "30px 20px" }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
            <Kicker index="01">The name</Kicker>
            {/* Position, not a promise: the name is the LAST step here. */}
            <span className="brm-count">2/2</span>
          </div>
          <h1 style={{ fontFamily: "var(--font-serif)", fontSize: 24, lineHeight: 1.3,
            letterSpacing: "-0.015em", margin: "18px 0 16px", color: "var(--ink-900)" }}>
            What should this board be called?
          </h1>
          <Input type="text" size="lg" fullWidth value={name} placeholder="VC Board" maxLength={MAX}
            onChange={(e) => setName(e.target.value.slice(0, MAX))} />
          {/* The counter appears only in the last eight characters. */}
          {name.length >= 40 && (
            <div className="brm-count" style={{ marginTop: 9 }}>{left} left</div>
          )}
          {error && (
            <div style={{ fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--critical)", marginTop: 10 }}>{error}</div>
          )}
        </div>
        <div className="brm-foot">
          <span className="brm-count">{count} of {cap || 8} seated</span>
          <Button size="lg" disabled={!trimmed || busy} onClick={() => onSave(trimmed)}>Seat the board</Button>
        </div>
      </React.Fragment>
    );
  }

  // ── 1e + 2d — the switcher. Lives in the dark bar; opens a menu panel, not a screen.
  // Tapping a board ACTIVATES it; tapping the one already active opens its
  // management screen. The spec asks for both from the same row ("reached from the
  // board's own row in the switcher", and "tap a board → it becomes active"), and
  // this is the reading that needs no second control on the row.
  function BoardSwitcher({ boards, activeBoardId, rosterCount, onPick, onManage, onAllMembers, onNewBoard }) {
    const [open, setOpen] = React.useState(false);
    const active = boards.filter((b) => b.id === activeBoardId)[0] || null;
    const close = () => setOpen(false);
    const pick = (fn) => () => { setOpen(false); fn(); };
    return (
      <React.Fragment>
        <style>{boardsCSS}</style>
        <button className="brm-swbtn" onClick={() => setOpen(true)} aria-haspopup="menu" aria-expanded={open}>
          {/* Zero boards: the bar states the truth — the full board answers. */}
          {active ? active.name : "The full board"}
          <Icon name="chevronDown" size={15} stroke="var(--claret-soft)" />
        </button>
        {open && (
          <React.Fragment>
            <button className="brm-scrim" aria-label="Close" onClick={close} />
            <div className="brm-panel" role="menu">
              <div className="brm-panel-h">Your boards</div>
              {boards.length === 0 ? (
                <div className="brm-full">
                  <div style={{ fontFamily: "var(--font-serif)", fontSize: 15, color: "var(--ink-900)" }}>The full board</div>
                  <div style={{ fontFamily: "var(--font-sans)", fontSize: 11.5, color: "var(--ink-500)", marginTop: 2 }}>
                    All {rosterCount} members answer
                  </div>
                </div>
              ) : (
                <div className="brm-list">
                  {boards.map((b) => (
                    <button key={b.id} role="menuitem"
                      className={"brm-brow" + (b.id === activeBoardId ? " brm-brow--on" : "")}
                      onClick={pick(() => (b.id === activeBoardId && onManage ? onManage(b.id) : onPick(b.id)))}>
                      <span className="brm-bname">{b.name}</span>
                      <span className="brm-bcount">{pad2(b.memberIds.length)}</span>
                    </button>
                  ))}
                </div>
              )}
              {/* 2d — the roster is reached from here. */}
              <div style={{ borderTop: "1px solid var(--line-faint)" }}>
                <button role="menuitem" className="brm-brow" onClick={pick(onAllMembers)}>
                  <span className="brm-bname">All members</span>
                  <span className="brm-bcount">{rosterCount}</span>
                </button>
              </div>
              <div style={{ borderTop: "1px solid var(--line)", margin: "6px 0 0" }}>
                <button role="menuitem" className="brm-new" onClick={pick(onNewBoard)}>
                  <Icon name="plus" size={15} stroke="var(--claret)" /> New board
                </button>
              </div>
            </div>
          </React.Fragment>
        )}
      </React.Fragment>
    );
  }

  // ── 5a–5d — Share as a LIST, one labelled link per person ──────────────────
  // SUPERSEDES round 4's single card. The unit is a PERSON: a link carries their
  // name and the board learns them under it across visits, so the block is a list.
  // Round 4's standing line, URL dialog and revoke dialog carry forward verbatim.
  //
  // THE PRONOUN RULE (Mike): a label is a name typed by the owner and gender is not
  // knowable from it, so every line and toast repeats the NAME. The spec's sample
  // copy ("his old one… still knows him") would have guessed. All of that copy is
  // built in logic.js so the no-pronoun guarantee is tested, not merely intended.
  //
  // `api` is the injected seam ({status, create, rotate, replace, forget, revoke}).
  function ShareBlock({ boardName, origin, api, onToast, onBackground }) {
    const { Button, Dialog, Kicker, IconButton, Input, Textarea } = DS;
    const [state, setState] = React.useState("loading");   // loading|ready|error
    const [links, setLinks] = React.useState([]);
    const [error, setError] = React.useState("");
    const [busy, setBusy] = React.useState(false);
    // Full URLs are held ONLY for this screen's lifetime, keyed by link id, so Copy
    // can write the whole thing after the one-time dialog is gone. Never persisted.
    const urlsRef = React.useRef({});
    const [freshIds, setFreshIds] = React.useState({});     // minted/rotated this session → "just now"
    const [naming, setNaming] = React.useState(null);       // {name, error} — beat 1
    const [beat2, setBeat2] = React.useState(null);         // {name, text} — the optional note
    const [minted, setMinted] = React.useState(null);       // {name, url} — beat 2
    const [sheet, setSheet] = React.useState(null);         // the row's action sheet
    const [confirm, setConfirm] = React.useState(null);     // {kind:"forget"|"revoke", link}

    const load = () => api.status()
      .then((s) => { setLinks((s && s.links) || []); setState("ready"); })
      .catch((e) => { setError(String(e.message || e)); setState("error"); });
    React.useEffect(() => { load(); }, []);

    const copy = (text) => {
      const w = (navigator.clipboard && navigator.clipboard.writeText)
        ? navigator.clipboard.writeText(text) : Promise.reject(new Error("no-clipboard"));
      return Promise.resolve(w).then(() => onToast(window.BR.SHARE_TOASTS.copied()))
        .catch(() => onToast("Couldn't copy the link — open it from the row and copy by hand."));
    };
    const remember = (linkId, token) => {
      const url = window.BR.guestUrl(origin, token);
      urlsRef.current[linkId] = url;
      setFreshIds((f) => Object.assign({}, f, { [linkId]: true }));
      return url;
    };

    // Beat 1 → beat 2. A duplicate label comes back 409 {kind:"label-in-use"}: that is
    // shown INLINE and the dialog stays open, because the fix is to edit the name.
    const createLink = (name, context) => {
      name = String(name || "").trim();
      if (!name || busy) return;
      setBusy(true);
      api.create(name, context || "")
        .then((r) => {
          const url = remember(r.link_id, r.token);
          setBeat2(null); setNaming(null);
          setMinted({ name: r.label || name, url: url });
          return load();
        })
        .catch((e) => {
          if (e && e.kind === "label-in-use") {
            // Back to beat 1, where the name is editable — the fix is the name, and
            // the note they just typed is carried so it is never lost to a retry.
            setBeat2(null);
            setNaming({ name: name, error: "A live link for " + name + " already exists", keepNote: context || "" });
          } else { onToast(String(e.message || e)); }
        })
        .then(() => setBusy(false));
    };

    // Rotate and replace fire with NO confirmation: each states its consequence
    // before the tap and confirms it after. Forget and revoke get dialogs.
    const runOp = (op, link) => {
      setSheet(null); setBusy(true);
      const T = window.BR.SHARE_TOASTS;
      const done = {
        rotate: () => onToast(T.rotated(link.label)),
        replace: () => onToast(T.replaced(link.label)),
        forget: () => onToast(T.forgotten(link.label)),
        revoke: () => {},
      }[op];
      api[op](link.link_id)
        .then((r) => {
          if (r && r.token) remember(r.link_id || link.link_id, r.token);
          if (op === "replace") delete urlsRef.current[link.link_id];
          done();
          return load();
        })
        .catch((e) => onToast(String(e.message || e)))
        .then(() => { setBusy(false); setConfirm(null); });
    };

    if (state === "loading") return null;
    const L = sheet ? window.BR.shareRowLines(sheet.label) : null;

    return (
      <div className="brm-share">
        <Kicker index={links.length ? pad2(links.length) : "§"}>Share</Kicker>
        <p className="brm-share-line">Anyone with the link can put questions to this board, without an account.</p>
        {state === "error" && (
          <div style={{ fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--critical)" }}>{error}</div>
        )}

        {links.map((lk) => (
          <div key={lk.link_id} className="brm-lrow">
            <div style={{ minWidth: 0, flex: 1 }}>
              <div className="brm-lname">{lk.label}</div>
              {/* Path only, fully masked — the row never re-exposes any of the token. */}
              <div className="brm-lstem">{window.BR.guestStemPath()}</div>
              <div className="brm-lmeta">
                {window.BR.shareMeta(lk.questions_today, lk.daily_question_cap, lk.created_at, !!freshIds[lk.link_id])}
              </div>
              {/* 6a — the SECOND meta line, present only when a tier actually exists.
                  `has_document`/`document_words` come from the backend; when it does not
                  expose them yet they are undefined, and backgroundMeta treats unknown as
                  NOT CLAIMED rather than as zero. */}
              {window.BR.backgroundMeta(!!(lk.context || "").trim(), lk.has_document, lk.document_words) && (
                <div className="brm-lmeta">
                  {window.BR.backgroundMeta(!!(lk.context || "").trim(), lk.has_document, lk.document_words)}
                </div>
              )}
            </div>
            {/* Two visible actions only — everything that changes or ends a link is
                behind the disclosure, so a row is never a button salad. */}
            <div className="brm-lacts">
              <Button size="sm" variant="secondary" disabled={busy || !urlsRef.current[lk.link_id]}
                onClick={() => copy(urlsRef.current[lk.link_id])}>Copy</Button>
              <IconButton label={"More for " + lk.label} variant="ghost" size="sm"
                onClick={() => setSheet(lk)}>
                <Icon name="moreVertical" size={17} stroke="currentColor" />
              </IconButton>
            </div>
          </div>
        ))}

        <div style={{ marginTop: 14 }}>
          <Button variant="secondary" fullWidth disabled={busy}
            onClick={() => setNaming({ name: "", error: "" })}>Create a link</Button>
        </div>
        {links.length > 0 && (
          <div className="brm-share-meta">Forty questions a day each, counted per day</div>
        )}

        {/* 5b beat 1 — the name, because the link is a person before it is a URL. */}
        {naming && (
          <Dialog open title="Who is this for?" onClose={() => setNaming(null)}
            footer={
              <React.Fragment>
                <Button variant="ghost" onClick={() => setNaming(null)}>Cancel</Button>
                <Button disabled={!naming.name.trim() || busy}
                  onClick={() => { setBeat2({ name: naming.name.trim(), text: "" }); setNaming(null); }}>Create the link</Button>
              </React.Fragment>
            }>
            Their name, so you know whose link this is. The board learns them under it across visits.
            <div style={{ marginTop: 14 }}>
              <Input type="text" size="lg" fullWidth value={naming.name} placeholder="Guy" maxLength={40}
                onChange={(e) => setNaming({ name: e.target.value.slice(0, 40), error: "" })} />
            </div>
            {naming.error && (
              <div style={{ fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--critical)", marginTop: 9 }}>
                {naming.error}
              </div>
            )}
          </Dialog>
        )}

        {/* 5b beat 2 — the background note. OPTIONAL, and the primary is identically
            labelled whether the field is empty or not: that IS the skip affordance.
            No skip link, no dismissal ceremony, no "you can add this later". */}
        {beat2 && (
          <Dialog open title="Anything the board should know?" onClose={() => setBeat2(null)}
            footer={
              <React.Fragment>
                <Button variant="ghost" disabled={busy}
                  onClick={() => { setNaming({ name: beat2.name, error: "" }); setBeat2(null); }}>Back</Button>
                <Button disabled={busy} onClick={() => createLink(beat2.name, beat2.text)}>Continue</Button>
              </React.Fragment>
            }>
            {window.BR.mintBgLine(beat2.name)}
            <div style={{ marginTop: 14 }}>
              <Textarea rows={4} value={beat2.text} maxLength={window.BR.NOTE_MAX}
                placeholder="My sister. Going through a divorce, two young kids."
                onChange={(e) => setBeat2(Object.assign({}, beat2,
                  { text: e.target.value.slice(0, window.BR.NOTE_MAX) }))} />
            </div>
            {beat2.text.length >= window.BR.NOTE_COUNTER_FROM && (
              <div className="brm-count" style={{ marginTop: 9 }}>
                {window.BR.NOTE_MAX - beat2.text.length} left
              </div>
            )}
          </Dialog>
        )}

        {/* 5b beat 3 — round 4's URL dialog, with the name in the title. */}
        {minted && (
          <Dialog open title={window.BR.possessive(minted.name) + " link to " + boardName}
            onClose={() => setMinted(null)}
            footer={
              <React.Fragment>
                <Button variant="ghost" onClick={() => setMinted(null)}>Done</Button>
                <Button onClick={() => copy(minted.url)}>Copy link</Button>
              </React.Fragment>
            }>
            Anyone holding it can put questions to this board. Copy it now — it is shown in full only here.
            <div className="brm-url-box"><div className="brm-url">{minted.url}</div></div>
          </Dialog>
        )}

        {/* 5c — the row sheet. Headed by whose link it is, so the target is never
            ambiguous, then the four operations with their teaching lines. */}
        {sheet && L && (
          <React.Fragment>
            <button className="brm-scrim" aria-label="Close" onClick={() => setSheet(null)} />
            <div className="brm-sheet" role="dialog" aria-modal="true">
              <div className="brm-sheet-h">{window.BR.possessive(sheet.label)} link</div>
              <div className="brm-lmeta" style={{ marginBottom: 6 }}>
                {window.BR.shareMeta(sheet.questions_today, sheet.daily_question_cap, sheet.created_at, !!freshIds[sheet.link_id])}
              </div>
              {["rotate", "replace", "background", "forget", "revoke"].map((op) => (
                <button key={op} className={"brm-sheet-row"
                    + (op === "revoke" ? " brm-sheet-row--critical" : "")
                    + (op === "background" ? " brm-sheet-row--door" : "")}
                  disabled={busy}
                  onClick={() => {
                    if (op === "rotate" || op === "replace") return runOp(op, sheet);
                    // The door PUSHES a screen — 30,000 characters do not belong in a modal
                    // (round 6 supersedes round 5's edit-later dialog).
                    if (op === "background") { setSheet(null); return onBackground(sheet); }
                    setConfirm({ kind: op, link: sheet }); setSheet(null);
                  }}>
                  <span style={{ flex: 1, minWidth: 0 }}>
                    <span className="brm-sheet-label">{L[op].label}</span>
                    <span className="brm-sheet-line">{L[op].line}</span>
                  </span>
                  {op === "background" && (
                    <Icon name="chevronRight" size={16} stroke="currentColor" className="brm-sheet-door-x" />
                  )}
                </button>
              ))}
            </div>
          </React.Fragment>
        )}

        {/* Forget destroys something even though access survives, so it confirms. */}
        {confirm && confirm.kind === "forget" && (
          <Dialog open title={"Forget " + confirm.link.label + "?"} onClose={() => setConfirm(null)}
            footer={
              <React.Fragment>
                <Button variant="ghost" onClick={() => setConfirm(null)}>Keep it</Button>
                <Button onClick={() => runOp("forget", confirm.link)}>Forget</Button>
              </React.Fragment>
            }>
            {window.BR.forgetBody(confirm.link.label)}
          </Dialog>
        )}

        {/* Revoke keeps round 4's dialog verbatim. */}
        {confirm && confirm.kind === "revoke" && (
          <Dialog open title="Revoke the link?" onClose={() => setConfirm(null)}
            footer={
              <React.Fragment>
                <Button variant="ghost" onClick={() => setConfirm(null)}>Keep it</Button>
                <Button onClick={() => runOp("revoke", confirm.link)}>Revoke</Button>
              </React.Fragment>
            }>
            Anyone holding it loses the board. You can create another later.
          </Dialog>
        )}
      </div>
    );
  }

  // ── 6b — the Background screen (a SCREEN, superseding round 5's dialog) ────
  // Two tiers, stated in their own lines and nowhere else. `docTier` is the honest
  // capability gate: the document surfaces render only when the BACKEND advertises
  // the tier (`has_document` present on the link). At the time of writing CC2's
  // document endpoints are live but UNCOMMITTED, so this is deliberately gated
  // rather than assumed — nothing here fakes a save that cannot happen.
  function BackgroundScreen({ link, docTier, api, onBack, onOpenDoc, onToast }) {
    const { Button, Kicker, Textarea } = DS;
    const name = link.label;
    const lines = window.BR.bgLines(name);
    const [note, setNote] = React.useState(link.context || "");
    const [busy, setBusy] = React.useState(false);
    const clean = note === (link.context || "");
    const save = () => {
      if (busy || clean) return;
      setBusy(true);
      api.context(link.link_id, note)
        .then(() => onToast("Background saved."))
        .catch((e) => onToast(String(e.message || e)))
        .then(() => setBusy(false));
    };
    return (
      <React.Fragment>
        <style>{boardsCSS}</style>
        <div style={{ padding: "14px 16px 0" }}>
          <Button variant="ghost" size="sm" onClick={onBack}>
            <Icon name="chevronLeft" size={15} stroke="currentColor" /> {"Background for " + name}
          </Button>
        </div>
        <div className="brm-bg">
          <Kicker index="01">The note</Kicker>
          <p className="brm-bg-line">{lines.note}</p>
          <Textarea rows={4} value={note} maxLength={window.BR.NOTE_MAX}
            onChange={(e) => setNote(e.target.value.slice(0, window.BR.NOTE_MAX))} />
          {note.length >= window.BR.NOTE_COUNTER_FROM && (
            <div className="brm-count" style={{ marginTop: 9 }}>{window.BR.NOTE_MAX - note.length} left</div>
          )}

          {docTier && (
            <React.Fragment>
              <div style={{ marginTop: 26 }}><Kicker index="02">The document</Kicker></div>
              <p className="brm-bg-line">{lines.doc}</p>
              {/* A ROW, not a field — filled shows which document it is; empty invites one. */}
              {link.has_document ? (
                <button className="brm-doc-card" onClick={onOpenDoc}>
                  <span style={{ flex: 1, minWidth: 0 }}>
                    <span className="brm-doc-words">
                      {window.BR.thousands(link.document_words || 0)} words
                    </span>
                    <span className="brm-doc-open">{link.document_opening || ""}</span>
                  </span>
                  <Icon name="chevronRight" size={16} stroke="var(--ink-300)" />
                </button>
              ) : (
                <button className="brm-doc-card brm-doc-empty" onClick={onOpenDoc}>
                  <span style={{ flex: 1 }}>
                    <span className="brm-doc-empty-l">Paste a document</span>
                    <span className="brm-doc-cap" style={{ display: "block" }}>Up to 5,000 words</span>
                  </span>
                </button>
              )}
            </React.Fragment>
          )}
        </div>
        {/* `Saved` only when clean; nothing at all while untouched. */}
        <div className="brm-bg-foot">
          <span className="brm-count">{clean && note ? "Saved" : ""}</span>
          <Button disabled={clean || busy} onClick={save}>Save</Button>
        </div>
      </React.Fragment>
    );
  }

  // ── 6c — the document. The WHOLE VIEWPORT is the text: no card, no chrome. ──
  function DocumentScreen({ link, api, onBack, onToast }) {
    const { Button, Dialog } = DS;
    const name = link.label;
    const [text, setText] = React.useState("");
    const [loaded, setLoaded] = React.useState(false);
    const [saved, setSaved] = React.useState("");
    const [busy, setBusy] = React.useState(false);
    const [confirmClear, setConfirmClear] = React.useState(false);
    React.useEffect(() => {
      api.documentGet(link.link_id)
        .then((r) => { const t = (r && r.document) || ""; setText(t); setSaved(t); setLoaded(true); })
        .catch(() => { setLoaded(true); onToast("Couldn't open the document just now."); });
    }, []);
    const words = window.BR.wordCount(text);
    const dirty = text !== saved;
    const save = () => {
      if (busy || !dirty) return;
      setBusy(true);
      api.document(link.link_id, text)
        .then(() => { setSaved(text); onToast("Background saved."); })
        .catch((e) => onToast(String(e.message || e)))
        .then(() => setBusy(false));
    };
    const clear = () => {
      setConfirmClear(false); setBusy(true);
      api.document(link.link_id, "")
        .then(() => { setText(""); setSaved(""); onToast("Document cleared."); })
        .catch((e) => onToast(String(e.message || e)))
        .then(() => setBusy(false));
    };
    return (
      <React.Fragment>
        <style>{boardsCSS}</style>
        <div className="brm-docbar">
          <Button variant="ghost" size="sm" onClick={onBack}>
            <Icon name="chevronLeft" size={15} stroke="currentColor" /> {window.BR.possessive(name) + " document"}
          </Button>
          <span className="brm-docbar-n">{window.BR.thousands(words)} words</span>
        </div>
        <div className="brm-docwrap">
          <textarea className="brm-doctext" value={text} disabled={!loaded}
            placeholder={loaded ? "Paste the document here." : ""}
            /* Paste beyond the cap TRUNCATES silently at 30,000 — the count is already
               showing the ceiling, so there is nothing to explain. No error, no dialog. */
            onChange={(e) => setText(window.BR.clampDoc(e.target.value))} />
        </div>
        <div className="brm-bg-foot">
          <span className="brm-count">{window.BR.docFooterCount(text)}</span>
          <span style={{ display: "flex", alignItems: "center", gap: 16 }}>
            {saved && <button className="brm-doc-clear" onClick={() => setConfirmClear(true)}>Clear</button>}
            <Button disabled={!dirty || busy} onClick={save}>Save</Button>
          </span>
        </div>
        {confirmClear && (
          <Dialog open title="Clear this document?" onClose={() => setConfirmClear(false)}
            footer={
              <React.Fragment>
                <Button variant="ghost" onClick={() => setConfirmClear(false)}>Keep it</Button>
                <Button onClick={clear}>Clear</Button>
              </React.Fragment>
            }>
            {window.BR.clearDocBody(name, window.BR.wordCount(saved))}
          </Dialog>
        )}
      </React.Fragment>
    );
  }

  // ── 1g — board management. Rename saves on blur; members are removed here and
  // added from the roster, pre-seeded.
  function BoardManageScreen({ board, experts, error, onBack, onRename, onRemove, onAddMembers, onDelete,
                               shareApi, shareOrigin, onToast }) {
    const { Button, Input, Avatar, IconButton, Kicker, Dialog } = DS;
    const MAX = 48;
    const [name, setName] = React.useState(board.name);
    const [confirming, setConfirming] = React.useState(false);
    const byId = {};
    experts.forEach((e) => { byId[e.id] = e; });
    // Unpublished members stay on the row but cannot answer; they are not drawn
    // here, because a row would assert a seat that no query will honour.
    const members = board.memberIds.map((id) => byId[id]).filter(Boolean);
    const blur = () => {
      const t = name.trim();
      if (!t || t === board.name) { setName(board.name); return; }
      onRename(t);
    };
    return (
      <React.Fragment>
        <style>{boardsCSS}</style>
        <div style={{ padding: "14px 16px 0" }}>
          <Button variant="ghost" size="sm" onClick={onBack}>
            <Icon name="chevronLeft" size={15} stroke="currentColor" /> Board
          </Button>
        </div>
        <div style={{ maxWidth: 720, margin: "0 auto", padding: "18px 20px 96px", width: "100%", boxSizing: "border-box" }}>
          {/* onBlur rides a wrapper, not the Input: the DS Input spreads `...rest` LAST, so
              an onBlur passed to it would replace its own focus-ring reset. Blur bubbles. */}
          <div onBlur={blur}>
            <Input type="text" size="lg" fullWidth value={name} maxLength={MAX}
              onChange={(e) => setName(e.target.value.slice(0, MAX))} />
          </div>
          {name.length >= 40 && <div className="brm-count" style={{ marginTop: 9 }}>{MAX - name.length} left</div>}
          {error && (
            <div style={{ fontFamily: "var(--font-sans)", fontSize: 13, color: "var(--critical)", marginTop: 10 }}>{error}</div>
          )}
          <div style={{ margin: "26px 0 4px" }}><Kicker index={pad2(members.length)}>Members</Kicker></div>
          {members.map((m) => (
            <div key={m.id} className="brm-mrow">
              <Avatar name={m.name} size="sm" />
              <div style={{ minWidth: 0, flex: 1 }}>
                <div className="brm-name" style={{ fontSize: 16 }}>{m.name}</div>
                <div className="brm-desc">{m.note}</div>
              </div>
              <IconButton label={"Remove " + m.name} variant="ghost" size="sm" onClick={() => onRemove(m.id)}>
                <Icon name="minus" size={15} stroke="currentColor" />
              </IconButton>
            </div>
          ))}
          <div style={{ marginTop: 18 }}>
            <Button variant="secondary" fullWidth onClick={onAddMembers}>Add members</Button>
          </div>
          {/* Share sits BETWEEN Add members and Delete (4a). Rendered only when the
              app wires the seam, so tests/older callers see management unchanged. */}
          {shareApi && (
            <ShareBlock boardName={board.name} origin={shareOrigin} api={shareApi} onToast={onToast} />
          )}
          <div style={{ borderTop: "1px solid var(--line)", marginTop: 26 }}>
            <button className="brm-danger" onClick={() => setConfirming(true)}>
              <Icon name="trash" size={15} stroke="var(--critical)" /> Delete this board
            </button>
          </div>
        </div>
        {confirming && (
          <Dialog open title={"Delete " + board.name + "?"} onClose={() => setConfirming(false)}
            footer={
              <React.Fragment>
                <Button variant="ghost" onClick={() => setConfirming(false)}>Keep it</Button>
                <Button onClick={() => { setConfirming(false); onDelete(); }}>Delete</Button>
              </React.Fragment>
            }>
            The sessions this board answered stay on the record. The board itself does not.
          </Dialog>
        )}
      </React.Fragment>
    );
  }

  return { AllMembersScreen, NameStepScreen, BoardSwitcher, BoardManageScreen, ShareBlock,
           BackgroundScreen, DocumentScreen, boardsCSS };
})();

Object.assign(window, {
  AllMembersScreen: BoardsUI.AllMembersScreen,
  NameStepScreen: BoardsUI.NameStepScreen,
  BoardSwitcher: BoardsUI.BoardSwitcher,
  BoardManageScreen: BoardsUI.BoardManageScreen,
  ShareBlock: BoardsUI.ShareBlock,
  BackgroundScreen: BoardsUI.BackgroundScreen,
  DocumentScreen: BoardsUI.DocumentScreen,
  boardsCSS: BoardsUI.boardsCSS,
});
