/* ============================================================
   Collaborative workshop — 3 conflict-resolution models
   ============================================================
   Models:
     A) soft-lock  — first to expand owns the Q. Others see read-only
                     lock screen with "Request takeover".
     B) optimistic — anyone can edit. Conflicts surface inline:
                     YNA pick conflict + downstream invalidation banner.
     C) assignment — Completer pre-assigns each Q. Only assignee can edit.

   Role:
     - Completer can submit. Collaborators cannot (submit button hidden).
   ============================================================ */

const { useState, useEffect, useRef, useMemo, useCallback, memo } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "conflictModel": "soft-lock",
  "role": "completer",
  "showRail": true,
  "showNav": true,
  "anim": "slide",
  "showRowAvatars": true,
  "showTypingIndicators": true
}/*EDITMODE-END*/;

/* ---------- Icons ---------- */
const I = {
  check:    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5"/></svg>,
  x:        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>,
  dash:     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M5 12h14"/></svg>,
  plus:     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M5 12h14"/></svg>,
  warn:     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>,
  user:     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>,
  flag:     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 22V4a2 2 0 0 1 2-2h11l-2 4 2 4H6"/></svg>,
  cal:      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg>,
  chevron:  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>,
  lock:     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>,
  bolt:     <svg viewBox="0 0 24 24" fill="currentColor"><path d="M13 2 4 14h7l-2 8 9-12h-7l2-8z"/></svg>,
  paperclip:<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 17.93 8.83l-8.49 8.49a2 2 0 1 1-2.83-2.83l8.49-8.48"/></svg>,
  dots:     <svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="19" cy="12" r="1.6"/></svg>,
  file:     <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>,
  trash:    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6"/><path d="M5 6l1 14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-14"/></svg>,
  download: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>,
  eye:      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>,
};

/* ---------- Users ---------- */
/* Avatar URLs fall back to pravatar when not running bundled. When bundled,
   window.__resources.av_* contains blob URLs to the inlined images. */
const _AV = (id, fallback) =>
  (typeof window !== "undefined" && window.__resources && window.__resources[id]) || fallback;
const ME = { id: "me",    name: "Andrew Matt",   avatar: _AV("av_me",     "https://i.pravatar.cc/64?img=12"), short: "AM" };
const USERS = [
  { id: "marcus", name: "Marcus Chen",    avatar: _AV("av_marcus", "https://i.pravatar.cc/64?img=32"), short: "MC", status: "online" },
  { id: "priya",  name: "Priya Iyer",     avatar: _AV("av_priya",  "https://i.pravatar.cc/64?img=23"), short: "PI", status: "online" },
  { id: "sarah",  name: "Sarah Mensah",   avatar: _AV("av_sarah",  "https://i.pravatar.cc/64?img=47"), short: "SM", status: "idle"   },
];
const userById = (id) => id === "me" ? ME : USERS.find(u => u.id === id);

/* ---------- Empty state ---------- */
function emptyAnswer() {
  return {
    q1: null, q2: null,
    q1NoFindings: "", q1NoAction: null,
    q2NoFindings: "", q2NoAction: null,
    otherComments: "",
    savedAt: null,
    lastEditedBy: null,
    // stashed data when invalidated by downstream conflict
    stash: null,
  };
}
function emptyAction() {
  return { title: "", desc: "", owner: "", priority: "med", due: "", tags: [], attachments: [], history: [] };
}

/* ---------- Derive completion state ---------- */
function deriveStatus(a) {
  if (a.q1 === null) return "empty";
  if (a.q1 === "na") return "done";
  if (a.q1 === "no") {
    const ok = a.q1NoFindings.trim() && a.q1NoAction && a.q1NoAction.title.trim();
    return ok ? "done" : "partial";
  }
  if (a.q2 === null) return "partial";
  if (a.q2 === "yes" || a.q2 === "na") return "done";
  if (a.q2 === "no") {
    const ok = a.q2NoFindings.trim() && a.q2NoAction && a.q2NoAction.title.trim();
    return ok ? "done" : "partial";
  }
  return "partial";
}

function answerSummary(a) {
  if (a.q1 === null) return null;
  if (a.q1 === "na") return { pills: [{ v: "na", label: "N/A" }] };
  if (a.q1 === "no") return { pills: [{ v: "no", label: "No" }], note: "Finding + action recorded" };
  if (a.q2 === null) return { pills: [{ v: "yes", label: "Yes" }], note: "Q2 pending" };
  if (a.q2 === "yes") return { pills: [{ v: "yes", label: "Yes" }, { v: "yes", label: "Evidence: Yes" }] };
  if (a.q2 === "na")  return { pills: [{ v: "yes", label: "Yes" }, { v: "na",  label: "Evidence: N/A" }] };
  if (a.q2 === "no")  return { pills: [{ v: "yes", label: "Yes" }, { v: "no",  label: "Evidence: No" }], note: "Finding + action recorded" };
  return null;
}

function relTime(ts) {
  if (!ts) return "";
  const s = Math.max(0, Math.floor((Date.now() - ts) / 1000));
  if (s < 5)   return "just now";
  if (s < 60)  return s + "s ago";
  if (s < 3600) return Math.floor(s / 60) + "m ago";
  return Math.floor(s / 3600) + "h ago";
}

/* ---------- YNA segmented (with other-user pick avatar) ---------- */
function YNA({ value, onChange, disabled, otherPicks }) {
  // otherPicks: { yes?: user, no?: user, na?: user } — collaborators who picked these
  const opts = [
    { v: "yes", label: "Yes", cls: "sel-yes", icon: I.check },
    { v: "no",  label: "No",  cls: "sel-no",  icon: I.x },
    { v: "na",  label: "N/A", cls: "sel-na",  icon: I.dash },
  ];
  return (
    <div className="yna-row" role="radiogroup">
      {opts.map(o => {
        const otherUser = otherPicks && otherPicks[o.v];
        return (
          <button
            key={o.v}
            type="button"
            role="radio"
            aria-checked={value === o.v}
            disabled={disabled}
            className={"yna" + (value === o.v ? " " + o.cls : "")}
            onClick={() => onChange(o.v)}
          >
            {value === o.v ? o.icon : null}
            {o.label}
            {otherUser && (
              <span className="other-pick" title={otherUser.name + " picked " + o.label}>
                <img src={otherUser.avatar} alt="" />
              </span>
            )}
          </button>
        );
      })}
    </div>
  );
}

/* ---------- Reveal ---------- */
function Reveal({ shown, anim, children }) {
  const ref = useRef(null);
  const [h, setH] = useState(0);
  useEffect(() => {
    if (!ref.current) return;
    const update = () => {
      const inner = ref.current.firstElementChild;
      if (inner) setH(inner.scrollHeight);
    };
    update();
    const ro = new ResizeObserver(update);
    if (ref.current.firstElementChild) ro.observe(ref.current.firstElementChild);
    return () => ro.disconnect();
  }, [children]);
  return (
    <div className="reveal" data-anim={anim} data-shown={shown ? "true" : "false"}
         ref={ref} style={{ "--reveal-h": (h + 4) + "px" }} aria-hidden={!shown}>
      <div className="reveal-inner">{children}</div>
    </div>
  );
}

/* ---------- Action card ---------- */
function ActionCard({ value, onChange, onRemove, keyId, disabled }) {
  const prio = { high: "High", med: "Medium", low: "Low" };
  return (
    <div className="action-card">
      <div className="action-card-head">
        <span className="key">{keyId}</span>
        <span className="domain"><span className="dot"></span>Action</span>
        <button type="button" className="close" disabled={disabled} aria-label="Remove" onClick={onRemove}>{I.x}</button>
      </div>
      <div className="action-card-body">
        <input type="text" className="title" disabled={disabled}
          placeholder="Action title — what needs to happen?"
          value={value.title}
          onChange={e => onChange({ ...value, title: e.target.value })} />
        <textarea className="desc" disabled={disabled}
          placeholder="Short description"
          value={value.desc}
          onChange={e => onChange({ ...value, desc: e.target.value })} />
        <div className="meta-row">
          <button type="button" className="meta-pill" disabled={disabled}
            data-empty={value.owner ? "false" : "true"}
            onClick={() => onChange({ ...value, owner: value.owner ? "" : "Sarah Mensah" })}>
            <span className="ico">{I.user}</span>
            <div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
              <span className="label">Owner</span>
              <span className="val">{value.owner || "Unassigned"}</span>
            </div>
          </button>
          <button type="button" className="meta-pill" disabled={disabled} data-priority={value.priority}
            onClick={() => {
              const o = ["low", "med", "high"];
              const next = o[(o.indexOf(value.priority) + 1) % o.length];
              onChange({ ...value, priority: next });
            }}>
            <span className="ico">{I.flag}</span>
            <div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
              <span className="label">Priority</span>
              <span className="val">{prio[value.priority]}</span>
            </div>
          </button>
          <button type="button" className="meta-pill" disabled={disabled}
            data-empty={value.due ? "false" : "true"}
            onClick={() => onChange({ ...value, due: value.due ? "" : "Jun 14, 2026" })}>
            <span className="ico">{I.cal}</span>
            <div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
              <span className="label">Due</span>
              <span className="val">{value.due || "No due date"}</span>
            </div>
          </button>
        </div>
      </div>
    </div>
  );
}

/* ---------- Action summary tile (clickable, opens modal) ---------- */
function ActionSummary({ value, onOpen, disabled, keyId }) {
  const prioLabel = { high: "High", med: "Medium", low: "Low" }[value.priority] || "Medium";
  const hasTitle = value.title && value.title.trim();
  return (
    <div
      className={"action-summary" + (disabled ? " disabled" : "")}
      onClick={() => { if (!disabled) onOpen(); }}
      role="button"
      tabIndex={disabled ? -1 : 0}
      onKeyDown={(e) => { if (!disabled && (e.key === "Enter" || e.key === " ")) { e.preventDefault(); onOpen(); } }}
    >
      <div className="action-summary-head">
        <span className="key">{keyId}</span>
        <span className="domain"><span className="dot"></span>Action</span>
        {!disabled && (
          <span className="edit-cue">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>
            Edit
          </span>
        )}
      </div>
      <div className="action-summary-body">
        <div className={"t" + (hasTitle ? "" : " placeholder")}>
          {hasTitle ? value.title : "Configure this action…"}
        </div>
        <div className="meta">
          <span className="chip" data-priority={value.priority}>{I.flag} {prioLabel}</span>
          <span className="sep"></span>
          <span className="chip">{I.user} {value.owner || "Unassigned"}</span>
          {value.attachments && value.attachments.length > 0 && (
            <>
              <span className="sep"></span>
              <span className="chip">{I.paperclip} {value.attachments.length} file{value.attachments.length === 1 ? "" : "s"}</span>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

/* ---------- Action modal (matches existing kit) ---------- */
function ActionModal({ value, onChange, onClose, onDelete, qNum, qText, keyId }) {
  const [draft, setDraft] = useState(value || emptyAction());
  const [savedAt, setSavedAt] = useState(null);
  const [menuOpenIdx, setMenuOpenIdx] = useState(null);
  const saveTimer = useRef(null);

  // Close attachment menu on outside click
  useEffect(() => {
    if (menuOpenIdx === null) return;
    const h = (e) => {
      if (!e.target.closest(".att-menu-wrap")) setMenuOpenIdx(null);
    };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [menuOpenIdx]);

  // Debounced autosave back to parent
  useEffect(() => {
    if (!saveTimer.current && JSON.stringify(draft) === JSON.stringify(value)) return;
    clearTimeout(saveTimer.current);
    saveTimer.current = setTimeout(() => {
      onChange(draft);
      setSavedAt(Date.now());
    }, 500);
    return () => clearTimeout(saveTimer.current);
  }, [draft]);

  // Esc to close
  useEffect(() => {
    const h = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", h);
    return () => document.removeEventListener("keydown", h);
  }, [onClose]);

  const update = (p) => setDraft(prev => ({ ...prev, ...p }));
  const cyclePriority = () => {
    const o = ["low", "med", "high"];
    update({ priority: o[(o.indexOf(draft.priority) + 1) % o.length] });
  };
  const prioMap = { high: "High", med: "Medium", low: "Low" };

  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <div className="modal-head">
          <div className="meta">
            <div className="modal-eyebrow"><span className="dot"></span>{keyId} · Action</div>
            <h3>Configure this action</h3>
            <div className="qref">Linked to <b>{qNum}</b> — {qText}</div>
          </div>
          <button type="button" className="modal-close" onClick={onClose} aria-label="Close">{I.x}</button>
        </div>

        <div className="modal-body">
          <div className="modal-main">
            <div>
              <label>Title</label>
              <input
                type="text" className="title"
                placeholder="Action title — what needs to happen?"
                value={draft.title}
                autoFocus
                onChange={e => update({ title: e.target.value })}
              />
            </div>
            <div>
              <label>Description</label>
              <textarea
                className="desc"
                placeholder="Explain what needs to happen, who's affected, and why this matters."
                value={draft.desc}
                onChange={e => update({ desc: e.target.value })}
              />
            </div>
            <div>
              <label>Attachments</label>
              <div className="att-grid">
                {(draft.attachments || []).map((att, i) => (
                  <div key={i} className="att-item">
                    <div className={"att-thumb " + (att.kind === "image" ? "image" : "file")}>
                      {att.kind === "image" && att.preview ? (
                        <div style={{
                          width: "100%", height: "100%",
                          background: att.preview,
                        }}></div>
                      ) : (
                        <>
                          <span className="att-glyph">{I.file}</span>
                          <span className="att-ext">{(att.name.split(".").pop() || "").toUpperCase()}</span>
                        </>
                      )}
                    </div>
                    <div className="att-meta">
                      <div className="att-name" title={att.name}>{att.name}</div>
                      <div className="att-menu-wrap">
                        <button type="button" className="att-menu-trigger" title="More"
                          onClick={(e) => {
                            e.stopPropagation();
                            setMenuOpenIdx(menuOpenIdx === i ? null : i);
                          }}>
                          {I.dots}
                        </button>
                        {menuOpenIdx === i && (
                          <div className="att-menu-pop" onClick={e => e.stopPropagation()}>
                            <button type="button" className="att-menu-opt"
                              onClick={() => { setMenuOpenIdx(null); }}>
                              <span className="ico">{I.download}</span>Download
                            </button>
                            <button type="button" className="att-menu-opt"
                              onClick={() => { setMenuOpenIdx(null); }}>
                              <span className="ico">{I.eye}</span>View
                            </button>
                            <button type="button" className="att-menu-opt danger"
                              onClick={() => {
                                update({ attachments: draft.attachments.filter((_, j) => j !== i) });
                                setMenuOpenIdx(null);
                              }}>
                              <span className="ico">{I.trash}</span>Delete
                            </button>
                          </div>
                        )}
                      </div>
                    </div>
                    <div className="att-size">{att.size}</div>
                  </div>
                ))}
                <button type="button" className="att-add"
                  onClick={() => {
                    const samples = [
                      { kind: "image", name: "site-walk-evidence.jpg", size: "2.4 MB", preview: "linear-gradient(135deg,#fbbf24 0%,#92400e 100%)" },
                      { kind: "file",  name: "Inspection report.pdf",  size: "486 KB" },
                      { kind: "image", name: "control-room-photo.png", size: "1.1 MB", preview: "linear-gradient(135deg,#34d399 0%,#065f46 100%)" },
                      { kind: "file",  name: "Findings summary.docx",  size: "112 KB" },
                    ];
                    const taken = (draft.attachments || []).map(a => a.name);
                    const next = samples.find(s => !taken.includes(s.name)) || samples[0];
                    update({ attachments: [...(draft.attachments || []), next] });
                  }}>
                  <span className="att-add-ico">{I.plus}</span>
                  <span className="att-add-label">Add file</span>
                </button>
              </div>
            </div>
            <div>
              <label>Tags</label>
              <div className="modal-tags">
                {(draft.tags || []).map((tag, i) => (
                  <span key={i} className="modal-tag">
                    {tag}
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
                      onClick={() => update({ tags: draft.tags.filter((_, j) => j !== i) })}>
                      <path d="M18 6 6 18M6 6l12 12"/>
                    </svg>
                  </span>
                ))}
                <button
                  type="button" className="modal-tag-add"
                  onClick={() => {
                    const tags = draft.tags || [];
                    const pool = ["Safety", "Leadership", "Process", "Training", "Audit"];
                    const next = pool.find(p => !tags.includes(p));
                    if (next) update({ tags: [...tags, next] });
                  }}
                >{I.plus} Add tag</button>
              </div>
            </div>

            {/* History — read-only audit trail */}
            <div className="modal-history">
              <div className="modal-history-h">History</div>
              <div className="modal-history-list">
                {(draft.history && draft.history.length > 0 ? draft.history : [
                  { who: "Andrew Matt", short: "AM", color: "#175cd3", verb: "created this action", at: Date.now() - 60_000 },
                ]).map((h, i) => (
                  <div key={i} className="history-item">
                    <span className="history-av" style={{ background: h.color || "#475467" }}>
                      {h.short}
                    </span>
                    <div className="history-body">
                      <div className="history-line">
                        <b>{h.who}</b> {h.verb}
                        {h.detail && <span className="history-detail"> — {h.detail}</span>}
                        .
                      </div>
                      <div className="history-when">{relTime(h.at)}</div>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </div>

          <div className="modal-side">
            <div className="modal-side-section">
              <div className="modal-side-h">Details</div>
              <button
                type="button" className="meta-pill"
                data-empty={draft.owner ? "false" : "true"}
                onClick={() => update({ owner: draft.owner ? "" : "Sarah Mensah" })}
              >
                <span className="ico">{I.user}</span>
                <div style={{ display: "flex", flexDirection: "column", minWidth: 0, alignItems: "flex-start" }}>
                  <span className="label">Owner</span>
                  <span className="val">{draft.owner || "Unassigned"}</span>
                </div>
              </button>
              <button
                type="button" className="meta-pill" data-priority={draft.priority}
                onClick={cyclePriority}
              >
                <span className="ico">{I.flag}</span>
                <div style={{ display: "flex", flexDirection: "column", minWidth: 0, alignItems: "flex-start" }}>
                  <span className="label">Priority</span>
                  <span className="val">{prioMap[draft.priority]}</span>
                </div>
              </button>
              <button
                type="button" className="meta-pill"
                data-empty={draft.due ? "false" : "true"}
                onClick={() => update({ due: draft.due ? "" : "Jun 14, 2026" })}
              >
                <span className="ico">{I.cal}</span>
                <div style={{ display: "flex", flexDirection: "column", minWidth: 0, alignItems: "flex-start" }}>
                  <span className="label">Due</span>
                  <span className="val">{draft.due || "No due date"}</span>
                </div>
              </button>
            </div>

            <div className="modal-side-section">
              <div className="modal-side-h">Status</div>
              {(() => {
                const hasTitle = draft.title.trim();
                const hasOwner = draft.owner.trim();
                const hasDue   = draft.due.trim();
                if (!hasTitle) {
                  return (
                    <span className="priority-pill" data-state="empty">
                      <span className="status-dot"></span>
                      Not started
                    </span>
                  );
                }
                if (!hasOwner || !hasDue) {
                  return (
                    <span className="priority-pill" data-state="progress">
                      <span className="status-dot"></span>
                      In draft · needs {!hasOwner && !hasDue ? "owner & due date" : (!hasOwner ? "owner" : "due date")}
                    </span>
                  );
                }
                return (
                  <span className="priority-pill" data-state="ready">
                    <span className="status-dot"></span>
                    Ready to track
                  </span>
                );
              })()}
            </div>
          </div>
        </div>

        <div className="modal-foot">
          <button type="button" className="delete" title="Remove action" onClick={onDelete}>
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M10 11v6M14 11v6"/><path d="M5 6l1 14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-14"/></svg>
          </button>
          <span className="saved-stamp">
            <span className="dot"></span>
            {savedAt ? <>Saved {relTime(savedAt)}</> : <>Autosaves as you type</>}
          </span>
          <span className="spacer"></span>
          <button className="btn btn-secondary" onClick={onClose}>Close</button>
          <button className="btn btn-primary" onClick={onClose}>Done</button>
        </div>
      </div>
    </div>
  );
}

/* ---------- Findings + action block ---------- */
function FindingsAndAction({ keyId, findings, action, setFindings, setAction, branch, disabled, typingByUser, onOpenActionModal, watchingModal }) {
  return (
    <div className={"nested " + branch}>
      <div className="field-block">
        <label className="field-label">
          Finding details <span className="req">*</span>
          {typingByUser && (
            <span className="typing-by">
              <span className="mav"><img src={typingByUser.avatar} alt=""/></span>
              {typingByUser.name.split(" ")[0]} typing
              <span className="dots"><i></i><i></i><i></i></span>
            </span>
          )}
        </label>
        <div className="field-help">Explain what's missing or why this control isn't met.</div>
        <textarea className="field" disabled={disabled}
          placeholder="e.g. Walks happen ad-hoc. No formal cadence, no follow-up register."
          value={findings}
          onChange={e => setFindings(e.target.value)} />
      </div>
      <div className="field-block">
        <label className="field-label">Action <span className="req">*</span> <span className="opt">· only one</span></label>

        {watchingModal ? (
          /* Watcher view — locker has the action modal open */
          <div className="action-modal-watching">
            <span className="av"><img src={watchingModal.avatar} alt=""/></span>
            <div className="body">
              <div className="title">
                {watchingModal.name.split(" ")[0]} is filling in this action in a popup
                <span className="typing-dots"><i></i><i></i><i></i></span>
              </div>
              <div className="sub">Updates will appear here once {watchingModal.name.split(" ")[0]} saves and closes the popup.</div>
            </div>
          </div>
        ) : !action ? (
          <button type="button" className="action-stub required" disabled={disabled}
            onClick={() => { setAction(emptyAction()); onOpenActionModal && onOpenActionModal(); }}>
            {I.plus} Add one action
          </button>
        ) : (
          <ActionSummary value={action} keyId={keyId} disabled={disabled}
            onOpen={() => onOpenActionModal && onOpenActionModal()} />
        )}
      </div>
    </div>
  );
}

function OtherComments({ value, onChange, disabled }) {
  return (
    <div className="field-block">
      <label className="field-label">Other comments <span className="opt">Optional</span></label>
      <textarea className="field" disabled={disabled}
        placeholder="What can be improved? What are we doing well?"
        value={value}
        onChange={e => onChange(e.target.value)} />
    </div>
  );
}

/* ============================================================
   QUESTION CARD CONTENT (the form fields)
   Disabled mode is used for read-only views in soft-lock + assignment models.
   ============================================================ */
function QuestionFields({ q, answer, setField, disabled, anim, otherPicksQ1, otherPicksQ2, typingByUser, onOpenActionModal, watchingModal }) {
  const showQ2     = answer.q1 === "yes";
  const showQ1No   = answer.q1 === "no";
  const showQ2No   = answer.q1 === "yes" && answer.q2 === "no";
  const showOtherC = answer.q1 === "yes" && (answer.q2 === "yes" || answer.q2 === "na");

  return (
    <>
      <div className="field-block">
        <label className="field-label">Do we have this in a process / system? <span className="req">*</span></label>
        <YNA value={answer.q1} disabled={disabled} otherPicks={otherPicksQ1}
          onChange={v => setField({ q1: v, ...(v !== "yes" ? { q2: null } : {}) })} />
      </div>

      <Reveal shown={showQ2} anim={anim}>
        <div className="nested yes">
          <div className="field-block">
            <label className="field-label">Do we have evidence? <span className="req">*</span></label>
            <YNA value={answer.q2} disabled={disabled} otherPicks={otherPicksQ2}
              onChange={v => setField({ q2: v })} />
          </div>
          <Reveal shown={showQ2No} anim={anim}>
            <FindingsAndAction
              keyId={"ACT-" + q.id.toUpperCase() + "-E"}
              findings={answer.q2NoFindings} action={answer.q2NoAction}
              setFindings={v => setField({ q2NoFindings: v })}
              setAction={v => setField({ q2NoAction: v })}
              branch="no" disabled={disabled}
              typingByUser={typingByUser && typingByUser.field === "q2NoFindings" ? typingByUser.user : null}
              onOpenActionModal={() => onOpenActionModal && onOpenActionModal("q2NoAction")}
              watchingModal={watchingModal && watchingModal.field === "q2NoAction" ? watchingModal.user : null}
            />
          </Reveal>
        </div>
      </Reveal>

      <Reveal shown={showQ1No} anim={anim}>
        <FindingsAndAction
          keyId={"ACT-" + q.id.toUpperCase()}
          findings={answer.q1NoFindings} action={answer.q1NoAction}
          setFindings={v => setField({ q1NoFindings: v })}
          setAction={v => setField({ q1NoAction: v })}
          branch="no" disabled={disabled}
          typingByUser={typingByUser && typingByUser.field === "q1NoFindings" ? typingByUser.user : null}
          onOpenActionModal={() => onOpenActionModal && onOpenActionModal("q1NoAction")}
          watchingModal={watchingModal && watchingModal.field === "q1NoAction" ? watchingModal.user : null}
        />
      </Reveal>

      <Reveal shown={showOtherC} anim={anim}>
        <OtherComments value={answer.otherComments} disabled={disabled}
          onChange={v => setField({ otherComments: v })} />
      </Reveal>
    </>
  );
}

/* ============================================================
   QUESTION ROW
   Branches by conflictModel for the expanded body and the row chrome.
   ============================================================ */
function QuestionRow({
  q, answer, setAnswer, expanded, onExpand, onCollapse,
  t, saveState, conflictModel, role,
  lockedBy, lockState, conflict, assignment, presenceOthers,
  requestTakeover, dismissConflict, dismissInvalidation, assignTo,
  typingByUser, flashClass, lockedDraft, lockerModalField,
  onOpenActionModal,
}) {
  const status = deriveStatus(answer);
  const summary = answerSummary(answer);
  const isLockedByOther = conflictModel === "soft-lock" && lockedBy && lockedBy !== "me";
  const isAssignedToOther = conflictModel === "assignment"
    && assignment && assignment !== "me";
  const isUnassignedAndCollab = conflictModel === "assignment"
    && !assignment && role === "collaborator";

  // Determine if read-only inside expanded
  const isReadOnly = isLockedByOther || isAssignedToOther || isUnassignedAndCollab;

  // Row classes
  const rowClasses = [
    "ws-row",
    expanded ? "expanded" : "collapsed",
    isLockedByOther ? "locked-by-other" : "",
    presenceOthers && presenceOthers.length ? "has-others" : "",
    conflictModel === "assignment" && assignment === "me" ? "assigned-to-me" : "",
    isReadOnly ? "read-only" : "",
    flashClass,
  ].filter(Boolean).join(" ");

  const setField = useCallback((p) => setAnswer({ ...answer, ...p }), [answer, setAnswer]);

  return (
    <div className={rowClasses}>
      <div className="ws-row-collapsed" onClick={() => {
        // Soft-lock: clicking a locked-by-other row still expands to show read-only state
        if (expanded) onCollapse(); else onExpand();
      }}>
        <div className={"marker " + (
          isLockedByOther
            ? (lockState === "disconnected" ? "locked-off" : (lockState === "idle" ? "locked-idle" : "locked"))
            : status === "done" ? "done"
            : status === "partial" ? "partial" : "empty"
        )}>
          {isLockedByOther && lockState === "disconnected" && I.warn}
          {isLockedByOther && lockState !== "disconnected" && I.lock}
          {!isLockedByOther && status === "done" && I.check}
          {!isLockedByOther && status === "partial" && I.warn}
        </div>
        <div className="ws-qnum">{q.num}</div>
        <div className="ws-qsummary">
          <div className="ws-qtext">{q.text}</div>
          <div className="ws-qsubsummary">
            {isLockedByOther && (
              <>
                <span className={"ws-pill " + (
                  lockState === "disconnected" ? "locked-off" :
                  lockState === "idle" ? "locked-idle" : "locked"
                )}>
                  {userById(lockedBy).name.split(" ")[0]}{" "}
                  {lockState === "disconnected" ? "disconnected"
                    : lockState === "idle" ? "idle"
                    : (<>is editing<span className="typing-dots" aria-hidden="true"><span></span><span></span><span></span></span></>)}
                </span>
              </>
            )}
            {!isLockedByOther && conflictModel === "assignment" && (
              <>
                {assignment ? (
                  <span className="ws-pill assigned">
                    Assigned to {userById(assignment).name.split(" ")[0]}
                  </span>
                ) : (
                  <span className="ws-pill muted">Unassigned</span>
                )}
                {summary && <span className="ws-dotsep"></span>}
              </>
            )}
            {!isLockedByOther && summary && summary.pills.map((p, i) => (
              <span key={i} className={"ws-pill " + p.v}>{p.label}</span>
            ))}
            {!isLockedByOther && summary && summary.note && (
              <>
                <span className="ws-dotsep"></span>
                <span>{summary.note}</span>
              </>
            )}
            {!summary && !isLockedByOther && conflictModel !== "assignment" && <span>{q.eyebrow}</span>}
          </div>
        </div>
        <div className="ws-row-aside">
          {t.showRowAvatars && presenceOthers && presenceOthers.length > 0 && (
            <span className="mini-av-stack">
              {presenceOthers.slice(0, 3).map(uid => {
                const u = userById(uid);
                const isTyping = typingByUser && typingByUser.user.id === uid;
                return (
                  <span key={uid} className={"mav" + (isTyping ? " typing" : "")} title={u.name}>
                    <img src={u.avatar} alt=""/>
                  </span>
                );
              })}
            </span>
          )}
          {saveState === "saving" && <span style={{ color: "#b54708" }}>Saving…</span>}
          {saveState === "saved" && answer.savedAt && (
            <span>
              Saved {relTime(answer.savedAt)}
              {answer.lastEditedBy && answer.lastEditedBy !== "me" && (
                <> by {userById(answer.lastEditedBy).name.split(" ")[0]}</>
              )}
            </span>
          )}
          <span className="chevron">{I.chevron}</span>
        </div>
      </div>

      <div className="ws-row-expand" data-shown={expanded ? "true" : "false"}>
        {expanded && (
          <>
            <div className="ws-row-expand-inner">
              <div>
                <div className="qhead-eyebrow">{q.eyebrow} · {q.num}</div>
                <h3 className="qhead-text">{q.text}</h3>
                <div className="qhead-example">{q.example}</div>
              </div>

              {/* --- Soft-lock: lock screen --- */}
              {isLockedByOther && (() => {
                const lockerUser = userById(lockedBy);
                const isIdle = lockState === "idle";
                const isOffline = lockState === "disconnected";
                const stateCls = isOffline ? "disconnected" : (isIdle ? "idle" : "");
                return (
                  <>
                    <div className={"lock-screen " + stateCls}>
                      <span className="lock-av"><img src={lockerUser.avatar} alt=""/></span>
                      <div className="lock-body">
                        <div className="title">
                          {isOffline
                            ? <>{lockerUser.name} left mid-answer</>
                            : isIdle
                              ? <>{lockerUser.name} hasn't typed in a while</>
                              : <>{lockerUser.name} is answering this question</>}
                        </div>
                        <div className="sub">
                          {isOffline
                            ? <>Their session ended without submitting. Their in-progress answer is below.</>
                            : isIdle
                              ? <>Their work is preserved below. You can request takeover if they've stepped away.</>
                              : <>Watch their progress below. Takeover is only available once they finish or step away.</>}
                        </div>
                        {(isIdle || isOffline) && (
                          <div className="lock-meta">
                            {isOffline ? I.warn : I.dash}
                            {isOffline
                              ? <>Disconnected · last save {relTime(answer && answer.savedAt)}</>
                              : <>Idle for 7 minutes · last typed at 14:32</>}
                          </div>
                        )}
                      </div>
                      <div className="lock-actions">
                        <button className="btn btn-secondary" onClick={onCollapse}>Skip</button>
                        {(isIdle || isOffline) && (
                          <button className="btn btn-purple" onClick={requestTakeover}>
                            {isOffline ? "Take over" : "Request takeover"}
                          </button>
                        )}
                      </div>
                    </div>
                    <div className="live-preview-banner">
                      <span className="lp-dot"></span>
                      <span className="lp-text">
                        {isOffline
                          ? <>Live preview · saved snapshot ({relTime(answer && answer.savedAt)})</>
                          : isIdle
                            ? <>Live preview · not currently typing</>
                            : <>Live preview · updating as {lockerUser.name.split(" ")[0]} types</>}
                      </span>
                      {answer && answer.savedAt && (
                        <span className="lp-time">Last save {relTime(answer.savedAt)}</span>
                      )}
                    </div>
                  </>
                );
              })()}

              {/* --- Assignment: read-only banner --- */}
              {conflictModel === "assignment" && isAssignedToOther && (
                <div className="read-only-banner">
                  {I.eye}
                  <span>This question is assigned to <span className="who">{userById(assignment).name}</span>. You can watch but not answer.</span>
                </div>
              )}
              {conflictModel === "assignment" && isUnassignedAndCollab && (
                <div className="read-only-banner">
                  {I.lock}
                  <span>This question hasn't been assigned. Ask the Completer to assign it before you can answer.</span>
                </div>
              )}
              {conflictModel === "assignment" && role === "completer" && !assignment && (
                <AssignControl onAssign={assignTo} />
              )}
              {conflictModel === "assignment" && role === "completer" && assignment && assignment !== "me" && (
                <div className="read-only-banner">
                  {I.user}
                  <span>Currently assigned to <span className="who">{userById(assignment).name}</span>.</span>
                  <button className="btn btn-secondary" style={{ marginLeft: "auto" }}
                    onClick={() => assignTo("me")}>Take over</button>
                  <button className="btn btn-secondary"
                    onClick={() => assignTo(null)}>Unassign</button>
                </div>
              )}

              {/* --- Optimistic: conflict banners --- */}
              {conflictModel === "optimistic" && conflict && conflict.type === "yna" && (
                <YnaConflictBanner conflict={conflict} onResolve={dismissConflict} setField={setField} />
              )}
              {conflictModel === "optimistic" && conflict && conflict.type === "invalidation" && (
                <InvalidationBanner conflict={conflict} onResolve={dismissInvalidation} />
              )}

              {/* --- Form fields --- */}
              <QuestionFields
                q={q}
                answer={isLockedByOther && lockedDraft ? lockedDraft : answer}
                setField={setField}
                disabled={isReadOnly}
                anim={t.anim}
                otherPicksQ1={conflictModel === "optimistic" ? buildOtherPicks(answer, "q1", presenceOthers) : null}
                otherPicksQ2={conflictModel === "optimistic" ? buildOtherPicks(answer, "q2", presenceOthers) : null}
                typingByUser={t.showTypingIndicators ? typingByUser : null}
                onOpenActionModal={(field) => onOpenActionModal && onOpenActionModal(q.id, field)}
                watchingModal={isLockedByOther && lockerModalField
                  ? { field: lockerModalField, user: userById(lockedBy) }
                  : null}
              />
            </div>

            <div className="ws-row-footer">
              <span className="hint">
                {saveState === "saving" && <><span className="dot" style={{ background: "#f79009" }}></span>Saving…</>}
                {saveState !== "saving" && answer.savedAt && <><span className="dot"></span>Saved {relTime(answer.savedAt)}{answer.lastEditedBy && answer.lastEditedBy !== "me" && <> by {userById(answer.lastEditedBy).name.split(" ")[0]}</>}</>}
                {!answer.savedAt && saveState !== "saving" && <>Autosaves as you answer</>}
              </span>
              <span className="spacer"></span>
              <button className="btn btn-secondary" onClick={onCollapse}>
                {isReadOnly ? "Close" : "Done — collapse"}
              </button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

function buildOtherPicks(answer, field, presenceOthers) {
  // For demo: we don't track each other-user's draft picks separately.
  // The conflict banner already shows the contradiction; we leave this empty
  // unless a conflict is active, in which case the bolt-sim provides the data.
  return {};
}

const MemoQuestionRow = memo(QuestionRow, (a, b) =>
  a.answer === b.answer && a.expanded === b.expanded && a.t === b.t &&
  a.saveState === b.saveState && a.conflictModel === b.conflictModel &&
  a.role === b.role && a.lockedBy === b.lockedBy && a.lockState === b.lockState &&
  a.conflict === b.conflict &&
  a.assignment === b.assignment && a.presenceOthers === b.presenceOthers &&
  a.typingByUser === b.typingByUser && a.flashClass === b.flashClass &&
  a.lockedDraft === b.lockedDraft && a.lockerModalField === b.lockerModalField
);

/* ---------- Assign control (model C) ---------- */
function AssignControl({ onAssign }) {
  return (
    <div className="read-only-banner" style={{ background: "#fff", borderStyle: "dashed" }}>
      {I.user}
      <span>Not assigned yet — pick who should answer this:</span>
      <span style={{ marginLeft: "auto", display: "inline-flex", gap: 6 }}>
        <button className="btn btn-secondary" onClick={() => onAssign("me")}>Me</button>
        {USERS.map(u => (
          <button key={u.id} className="btn btn-secondary" onClick={() => onAssign(u.id)}>
            {u.name.split(" ")[0]}
          </button>
        ))}
      </span>
    </div>
  );
}

/* ---------- Conflict banners ---------- */
function YnaConflictBanner({ conflict, onResolve, setField }) {
  const u = userById(conflict.by);
  const myPickLabel = { yes: "Yes", no: "No", na: "N/A" }[conflict.myPick];
  const theirPickLabel = { yes: "Yes", no: "No", na: "N/A" }[conflict.theirPick];
  const myCls = conflict.myPick === "yes" ? "y" : conflict.myPick === "no" ? "n" : "na";
  const theirCls = conflict.theirPick === "yes" ? "y" : conflict.theirPick === "no" ? "n" : "na";
  return (
    <div className="yna-conflict">
      {I.warn}
      <span className="msg">
        <b>{u.name}</b> picked <span className={"pick " + theirCls}>{theirPickLabel}</span> for {conflict.field === "q1" ? "Q1" : "Q2"} — you have <span className={"pick " + myCls}>{myPickLabel}</span>
      </span>
      <span className="actions">
        <button onClick={() => { setField({ [conflict.field]: conflict.theirPick, ...(conflict.field === "q1" && conflict.theirPick !== "yes" ? { q2: null } : {}) }); onResolve(); }}>Use {u.name.split(" ")[0]}'s</button>
        <button className="primary" onClick={onResolve}>Keep mine</button>
      </span>
    </div>
  );
}

function InvalidationBanner({ conflict, onResolve }) {
  const u = userById(conflict.by);
  return (
    <div className="invalidation-banner">
      <svg className="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
      <div className="body">
        <div className="title">{u.name} changed Q1 — your in-progress answer was hidden</div>
        <div>{u.name.split(" ")[0]} set Q1 to <b>{conflict.newQ1.toUpperCase()}</b>, which makes the fields you were filling no longer applicable. Your work is preserved below — restore it any time.</div>
        <div className="stash">
          <b>Stashed finding:</b> {conflict.stashed.q2NoFindings || "(empty)"}
          {conflict.stashed.q2NoAction && conflict.stashed.q2NoAction.title && (
            <><br/><b>Stashed action:</b> {conflict.stashed.q2NoAction.title}</>
          )}
        </div>
        <div className="actions">
          <button className="primary" onClick={() => onResolve({ accept: "theirs" })}>Accept {u.name.split(" ")[0]}'s change</button>
          <button onClick={() => onResolve({ accept: "mine" })}>Undo their change · restore my work</button>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   LEFT NAVIGATOR
   ============================================================ */
function Navigator({ questions, answers, expandedId, locks, lockStates, onJumpToQ, done, total }) {
  // Group questions by eyebrow
  const groups = useMemo(() => {
    const map = new Map();
    questions.forEach(q => {
      if (!map.has(q.eyebrow)) map.set(q.eyebrow, []);
      map.get(q.eyebrow).push(q);
    });
    return Array.from(map.entries()).map(([eyebrow, items]) => ({
      // Eyebrow is "Section · Subsection" — show just the subsection (after the · ) for compactness
      title: eyebrow.includes(" · ") ? eyebrow.split(" · ")[1] : eyebrow,
      items,
    }));
  }, [questions]);

  const [collapsed, setCollapsed] = useState({});
  const pct = Math.round((done / total) * 100);

  // Scroll active item into view when expandedId changes
  const scrollRef = useRef(null);
  useEffect(() => {
    if (!expandedId || !scrollRef.current) return;
    const el = scrollRef.current.querySelector('[data-q="' + expandedId + '"]');
    if (el) {
      const r = el.getBoundingClientRect();
      const sr = scrollRef.current.getBoundingClientRect();
      if (r.top < sr.top + 40 || r.bottom > sr.bottom - 40) {
        el.scrollIntoView({ block: "center", behavior: "smooth" });
      }
    }
  }, [expandedId]);

  return (
    <div className="ws-nav">
      <div className="ws-nav-body" ref={scrollRef}>
        {groups.map(g => {
          const doneInGroup = g.items.filter(q => deriveStatus(answers[q.id]) === "done").length;
          const isCollapsed = !!collapsed[g.title];
          return (
            <div key={g.title} className={"ws-nav-section" + (isCollapsed ? " collapsed" : "")}>
              <div className="ws-nav-section-h" onClick={() => setCollapsed(s => ({ ...s, [g.title]: !s[g.title] }))}>
                <svg className="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>
                <span className="label">{g.title}</span>
                <span className="count"><b>{doneInGroup}</b>/{g.items.length}</span>
              </div>
              <div className="ws-nav-items">
                {g.items.map(q => {
                  const status = deriveStatus(answers[q.id]);
                  const lockedBy = locks[q.id];
                  const lockState = lockStates[q.id] || "active";
                  const isLockedByOther = lockedBy && lockedBy !== "me";
                  let iconCls = "empty";
                  if (isLockedByOther) {
                    iconCls = lockState === "disconnected" ? "locked-off"
                            : lockState === "idle" ? "locked-idle" : "locked";
                  } else if (status === "done") iconCls = "done";
                  else if (status === "partial") iconCls = "partial";
                  return (
                    <div
                      key={q.id}
                      data-q={q.id}
                      className={"ws-nav-item" + (expandedId === q.id ? " active" : "") + (isLockedByOther ? " locked-by-other" : "")}
                      onClick={() => onJumpToQ(q.id)}
                      title={q.text}
                    >
                      <div className={"icon " + iconCls}>
                        {iconCls === "done" && I.check}
                        {iconCls === "partial" && I.warn}
                        {(iconCls === "locked" || iconCls === "locked-idle") && I.lock}
                        {iconCls === "locked-off" && I.warn}
                      </div>
                      <div className="num">{q.num}</div>
                      <div className="lbl">{q.text}</div>
                      {isLockedByOther && (
                        <span className="live-av" title={userById(lockedBy).name + " is editing"}>
                          <img src={userById(lockedBy).avatar} alt=""/>
                        </span>
                      )}
                      {!isLockedByOther && expandedId === q.id && (
                        <span className="me-here" title="You're here"></span>
                      )}
                    </div>
                  );
                })}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ============================================================
   SESSION RAIL  (right column)
   ============================================================ */
function SessionRail({ activities, conflictModel, role, sims, presenceMap, onJumpToQ, questions }) {
  const [simOpen, setSimOpen] = useState(false);
  return (
    <div className="ws-rail">
      <div className="rail-h">
        <span className="pulse"></span>
        In this session · {USERS.length + 1} people
      </div>

      <div className="rail-section">
        <div className="rail-user">
          <span className="av">
            <img src={ME.avatar} alt=""/>
            <span className="dot dot-online"></span>
          </span>
          <div className="body">
            <div className="name">{ME.name} <span style={{ color: "var(--text-tertiary)", fontWeight: 400 }}>(you)</span></div>
            <div className="activity">You · viewing list</div>
          </div>
          {role === "completer" && <span className="role-mark">Completer</span>}
        </div>
        {USERS.map(u => {
          const act = activities[u.id];
          return (
            <div key={u.id} className="rail-user">
              <span className="av">
                <img src={u.avatar} alt=""/>
                <span className={"dot " + (u.status === "online" ? "dot-online" : "dot-idle")}></span>
              </span>
              <div className="body" style={{ minWidth: 0 }}>
                <div className="name">{u.name}</div>
                {act ? (
                  <>
                    <div className="activity">{act.typing ? "Typing in" : "Editing"} {act.qNum}</div>
                    <button
                      type="button"
                      className="on-chip"
                      onClick={() => onJumpToQ(act.qId)}
                      title={"Jump to " + act.qNum + " — " + act.qText}
                    >
                      {I.eye}
                      <span className="qn">{act.qNum}</span>
                      <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 130 }}>{act.qText}</span>
                      <span className="arrow">›</span>
                    </button>
                  </>
                ) : (
                  <div className="activity">{u.status === "idle" ? "Idle · 5m" : "Viewing list"}</div>
                )}
              </div>
            </div>
          );
        })}
      </div>

      <button type="button" className="rail-sim-toggle"
        aria-expanded={simOpen}
        onClick={() => setSimOpen(o => !o)}>
        <span className="lbl">{I.bolt} Simulate conflict</span>
        <svg className="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>
      </button>
      <div className="rail-sim-body" aria-hidden={!simOpen}>
        {conflictModel === "soft-lock" && (
          <>
            <button className="sim-btn compact" onClick={sims.lockFirstUnanswered}>
              <span className="t"><span className="av-tiny"><img src={USERS[0].avatar} alt=""/></span>Marcus opens an unanswered Q</span>
              <span className="s">Toast + row turns purple.</span>
            </button>
            <button className="sim-btn compact" onClick={sims.marcusOpenModal}>
              <span className="t"><span className="av-tiny"><img src={USERS[0].avatar} alt=""/></span>Toggle Marcus's action modal</span>
              <span className="s">Violet placeholder for watchers.</span>
            </button>
            <button className="sim-btn compact" onClick={sims.markIdle}>
              <span className="t">Mark Marcus idle (5+ min)</span>
              <span className="s">Lock turns amber.</span>
            </button>
            <button className="sim-btn compact" onClick={sims.markDisconnected}>
              <span className="t">Marcus disconnects</span>
              <span className="s">Lock turns red, takeover offered.</span>
            </button>
            <button className="sim-btn compact" onClick={sims.releaseLocks}>
              <span className="t">Release all locks</span>
              <span className="s"></span>
            </button>
          </>
        )}
        {conflictModel === "optimistic" && (
          <>
            <button className="sim-btn compact" onClick={sims.ynaConflict}>
              <span className="t"><span className="av-tiny"><img src={USERS[0].avatar} alt=""/></span>Marcus changes Q1 answer</span>
              <span className="s">Inline conflict banner.</span>
            </button>
            <button className="sim-btn compact" onClick={sims.invalidation}>
              <span className="t"><span className="av-tiny"><img src={USERS[0].avatar} alt=""/></span>Marcus flips Q1 → N/A</span>
              <span className="s">Set Q1=Yes Q2=No first.</span>
            </button>
            <button className="sim-btn compact" onClick={sims.startTyping}>
              <span className="t"><span className="av-tiny"><img src={USERS[1].avatar} alt=""/></span>Priya starts typing</span>
              <span className="s">In a No-branch findings field.</span>
            </button>
            <button className="sim-btn compact" onClick={sims.stopTyping}>
              <span className="t">Stop typing</span>
              <span className="s"></span>
            </button>
          </>
        )}
        {conflictModel === "assignment" && (
          <>
            <button className="sim-btn compact" onClick={sims.assignSampleQuestions}>
              <span className="t">Assign 10 Qs to Marcus & Priya</span>
              <span className="s"></span>
            </button>
            <button className="sim-btn compact" onClick={sims.clearAssignments}>
              <span className="t">Clear all assignments</span>
              <span className="s"></span>
            </button>
          </>
        )}
      </div>
    </div>
  );
}

/* ============================================================
   ActionsView — flat list of every action created so far,
   filterable by priority and state. Each row opens the same
   ActionModal as the question rows, and links back to its
   source question.
   ============================================================ */
function ActionsView({ questions, answers, onOpenAction, onJumpToQ }) {
  const [filter, setFilter] = useState("all");

  const allActions = useMemo(() => {
    const list = [];
    questions.forEach(q => {
      const a = answers[q.id];
      if (a.q1NoAction) list.push({
        qId: q.id, qNum: q.num, qText: q.text,
        field: "q1NoAction",
        keyId: "ACT-" + q.id.toUpperCase(),
        trigger: "Standard not met",
        action: a.q1NoAction,
      });
      if (a.q2NoAction) list.push({
        qId: q.id, qNum: q.num, qText: q.text,
        field: "q2NoAction",
        keyId: "ACT-" + q.id.toUpperCase() + "-E",
        trigger: "No evidence",
        action: a.q2NoAction,
      });
    });
    return list;
  }, [questions, answers]);

  const counts = useMemo(() => ({
    all:        allActions.length,
    high:       allActions.filter(x => x.action.priority === "high").length,
    med:        allActions.filter(x => x.action.priority === "med").length,
    low:        allActions.filter(x => x.action.priority === "low").length,
    draft:      allActions.filter(x => !x.action.title || !x.action.title.trim()).length,
    configured: allActions.filter(x => x.action.title && x.action.title.trim()).length,
  }), [allActions]);

  const filtered = useMemo(() => {
    if (filter === "all") return allActions;
    if (filter === "high" || filter === "med" || filter === "low") {
      return allActions.filter(x => x.action.priority === filter);
    }
    if (filter === "draft")      return allActions.filter(x => !x.action.title || !x.action.title.trim());
    if (filter === "configured") return allActions.filter(x => x.action.title && x.action.title.trim());
    return allActions;
  }, [allActions, filter]);

  if (allActions.length === 0) {
    return (
      <div className="actions-empty">
        <div className="ico">{I.flag}</div>
        <h3>No actions yet</h3>
        <p>Actions show up here as you raise findings. Whenever you answer <b>No</b> to a question, you'll be asked to create one action to close the gap.</p>
      </div>
    );
  }

  const prioLabel = { high: "High", med: "Medium", low: "Low" };

  return (
    <div className="actions-view">
      <div className="actions-toolbar">
        <div className="actions-filters">
          {[
            ["all",        "All",        counts.all],
            ["high",       "High",       counts.high],
            ["med",        "Medium",     counts.med],
            ["low",        "Low",        counts.low],
            ["configured", "Configured", counts.configured],
            ["draft",      "Draft",      counts.draft],
          ].map(([key, label, n]) => (
            <button key={key} type="button"
              className={"actions-chip" + (filter === key ? " on" : "")}
              onClick={() => setFilter(key)}>
              {label}
              <span className="n">{n}</span>
            </button>
          ))}
        </div>
        <div className="actions-summary-line">
          Showing <b>{filtered.length}</b> of <b>{allActions.length}</b>
        </div>
      </div>

      <div className="actions-list">
        {filtered.map(item => {
          const a = item.action;
          const hasTitle = a.title && a.title.trim();
          const prio = a.priority || "med";
          /* Look up the owner's avatar from the USERS table so the row shows
             a person, not just a name. Falls back to a deterministic pravatar
             slot keyed on the name so unknown owners still get a face. */
          const ownerUser = USERS.find(u => u.name === a.owner);
          const ownerAvatar = ownerUser ? ownerUser.avatar
                            : a.owner ? `https://i.pravatar.cc/64?u=${encodeURIComponent(a.owner)}`
                            : null;
          /* Derive a status pill from the data shape — collab actions don't
             carry an explicit status field. Untitled or unowned actions read
             as "Open"; fully-configured ones read as "In progress". */
          const status = !hasTitle ? "open"
                       : !a.owner   ? "open"
                       :              "in-progress";
          const statusLabel = status === "open" ? "Open" : "In progress";
          return (
            <div key={item.keyId + ":" + item.qId} className="action-row"
              role="button" tabIndex={0}
              onClick={() => onOpenAction(item.qId, item.field)}
              onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onOpenAction(item.qId, item.field); } }}>
              <div className="action-main">
                <div className="action-key-row">
                  <span className="key">{item.keyId}</span>
                </div>
                <div className="action-title-row">
                  <h4 className={"action-title" + (hasTitle ? "" : " placeholder")}>
                    {hasTitle ? a.title : "Untitled action — click to configure"}
                  </h4>
                  <span className={"prio-chip prio-" + prio}>{prioLabel[prio]} priority</span>
                </div>
                {a.desc && a.desc.trim() && (
                  <p className="action-desc">{a.desc}</p>
                )}
                <div className="action-tags">
                  {(a.tags || []).map((t, i) => <span key={i} className="tag-sm">{t}</span>)}
                  {a.attachments && a.attachments.length > 0 && (
                    <span className="tag-sm tag-attachment">
                      {I.paperclip}
                      {a.attachments.length} file{a.attachments.length === 1 ? "" : "s"}
                    </span>
                  )}
                  <button type="button" className="tag-sm tag-linked"
                    onClick={(e) => { e.stopPropagation(); onJumpToQ(item.qId); }}
                    title={"Jump to " + item.qNum}>
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
                    Linked to {item.qNum}
                  </button>
                </div>
              </div>
              <div className="action-meta">
                <div className={"action-owner" + (a.owner ? "" : " unassigned")}>
                  {a.owner
                    ? <>
                        <img src={ownerAvatar} alt="" />
                        <span>{a.owner}</span>
                      </>
                    : <span>Unassigned</span>}
                </div>
                {a.due && <div className="action-due">Due {a.due}</div>}
                <div className={"status-pill is-" + status}>{statusLabel}</div>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ============================================================
   APP
   ============================================================ */
function App() {
  const [t, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
  const questions = window.WORKSHOP_QUESTIONS;

  const [answers, setAnswers] = useState(() => {
    const seed = {};
    questions.forEach(q => seed[q.id] = emptyAnswer());

    // Seed a handful of actions across a few questions so the Actions tab
    // has something to show on first load. Mix of priorities, owners, and
    // draft / configured states.
    const setAction = (qId, field, q1, action, findings, q2) => {
      if (!seed[qId]) return;
      seed[qId] = {
        ...seed[qId],
        q1: q1 || seed[qId].q1,
        q2: q2 !== undefined ? q2 : seed[qId].q2,
        [field === "q1NoAction" ? "q1NoFindings" : "q2NoFindings"]: findings,
        [field]: action,
        savedAt: Date.now(),
      };
    };

    setAction("q001", "q1NoAction", "no",
      { title: "Publish leadership commitment statement and circulate to all sites",
        desc: "Draft a one-page signed statement covering safety, environment and community commitments.",
        owner: "Marcus Chen", priority: "high", due: "", tags: ["Leadership", "Process"],
        attachments: [
          { kind: "image", name: "site-walk-evidence.jpg", size: "2.4 MB", preview: "linear-gradient(135deg,#fbbf24 0%,#92400e 100%)" },
          { kind: "file",  name: "Draft commitment.docx",   size: "112 KB" },
        ],
        history: [
          { who: "Andrew Matt",  short: "AM", color: "#175cd3", verb: "created this action",         at: Date.now() - 3 * 86_400_000 },
          { who: "Marcus Chen",  short: "MC", color: "#067647", verb: "set the owner",                detail: "Marcus Chen", at: Date.now() - 2 * 86_400_000 - 4_000_000 },
          { who: "Andrew Matt",  short: "AM", color: "#175cd3", verb: "raised priority to High",      at: Date.now() - 2 * 86_400_000 },
          { who: "Marcus Chen",  short: "MC", color: "#067647", verb: "attached site-walk-evidence.jpg", at: Date.now() - 1 * 86_400_000 - 8_000_000 },
          { who: "Marcus Chen",  short: "MC", color: "#067647", verb: "attached Draft commitment.docx",  at: Date.now() - 1 * 86_400_000 },
        ],
      },
      "No signed leadership statement currently on file across the operations.");

    setAction("q002", "q1NoAction", "no",
      { title: "Add safety performance review to monthly exec agenda",
        desc: "", owner: "Priya Iyer", priority: "med", due: "" },
      "Exec meetings happen monthly but safety is not a standing item.");

    setAction("q003", "q2NoAction", "yes",
      { title: "Re-run site walk programme with formal cadence",
        desc: "Quarterly target with sign-off log; current walks are ad-hoc.",
        owner: "", priority: "high", due: "", tags: ["Safety"],
        attachments: [
          { kind: "image", name: "control-room-photo.png", size: "1.1 MB", preview: "linear-gradient(135deg,#34d399 0%,#065f46 100%)" },
        ] },
      "Walks happen but no formal cadence or sign-off log.", "no");

    setAction("q005", "q1NoAction", "no",
      { title: "", desc: "", owner: "", priority: "med", due: "" },
      "Risk register exists but no documented review schedule.");

    setAction("q007", "q1NoAction", "no",
      { title: "Standardise contractor onboarding checklist",
        desc: "One checklist across all contractor categories; track completion in the LMS.",
        owner: "Sarah Mensah", priority: "med", due: "", tags: ["Training", "Audit"],
        attachments: [
          { kind: "file", name: "Inspection report.pdf",  size: "486 KB" },
          { kind: "file", name: "Onboarding checklist.xlsx", size: "34 KB" },
          { kind: "image", name: "contractor-yard.jpg", size: "3.6 MB", preview: "linear-gradient(135deg,#60a5fa 0%,#1e3a8a 100%)" },
        ],
        history: [
          { who: "Sarah Mensah", short: "SM", color: "#6941c6", verb: "created this action",         at: Date.now() - 5 * 86_400_000 },
          { who: "Priya Iyer",   short: "PI", color: "#b54708", verb: "attached Inspection report.pdf",   at: Date.now() - 3 * 86_400_000 },
          { who: "Sarah Mensah", short: "SM", color: "#6941c6", verb: "attached Onboarding checklist.xlsx", at: Date.now() - 1 * 86_400_000 - 2_000_000 },
          { who: "Andrew Matt",  short: "AM", color: "#175cd3", verb: "attached contractor-yard.jpg",       at: Date.now() - 7_000_000 },
        ],
      },
      "Each site uses its own contractor onboarding form. No consolidated view.");

    setAction("q009", "q2NoAction", "yes",
      { title: "Quarterly emergency drill across all shafts",
        desc: "", owner: "Marcus Chen", priority: "low", due: "" },
      "Drills happen but inconsistently and not all shafts participate.", "no");

    setAction("q012", "q1NoAction", "no",
      { title: "Roll out incident-reporting mobile app to frontline crews",
        desc: "Phase 1: top 3 sites; Phase 2: remaining operations.",
        owner: "Priya Iyer", priority: "high", due: "", tags: ["Process", "Safety"],
        attachments: [
          { kind: "file", name: "App rollout plan.pdf", size: "1.2 MB" },
        ] },
      "Reporting is paper-based and lags 2-3 days behind events.");

    setAction("q015", "q1NoAction", "no",
      { title: "", desc: "", owner: "", priority: "low", due: "" },
      "PPE inspection records held in spreadsheets, not auditable.");

    return seed;
  });
  const [saveStates, setSaveStates] = useState({});
  const [expandedId, setExpandedId] = useState(questions[0].id);
  const timersRef = useRef({});

  // Collab state
  const [locks, setLocks] = useState({});           // {qId: userId}
  const [lockStates, setLockStates] = useState({}); // {qId: 'active'|'idle'|'disconnected'}
  const [conflicts, setConflicts] = useState({});   // {qId: {type, ...}}
  const [assignments, setAssignments] = useState({}); // {qId: userId | null}
  const [typingMap, setTypingMap] = useState({});   // {qId: {user, field}}
  // Drafts being typed by other users (rendered as live read-only preview
  // beneath the lock screen). Keyed by qId.
  const [othersDrafts, setOthersDrafts] = useState({});
  // Which question + field has the OTHER user's action modal open (watcher view)
  const [othersModalOpen, setOthersModalOpen] = useState({}); // {qId: 'q1NoAction' | 'q2NoAction'}
  // Which action seemed in the modal — same as before but also remember
  // the tab to switch back to after closing.
  const [myModal, setMyModal] = useState(null); // { qId, field } | null

  // Top-level view tab
  const [activeTab, setActiveTab] = useState("questions"); // 'questions' | 'actions'

  // Pre-stage some live collaborator activity so the lock UX is visible
  // immediately on page load (instead of hiding behind a sim button).
  // Only runs once on mount, and skips initial-load toasts so the user
  // isn't ambushed by 2 notifications they didn't trigger.
  const didSeedRef = useRef(false);
  useEffect(() => {
    if (didSeedRef.current) return;
    didSeedRef.current = true;
    if (t.conflictModel === "soft-lock") {
      // Marcus locks the 5th question, Priya locks the 12th
      const marcusQ = questions[4];   // ~1.1.5
      const priyaQ  = questions[11];  // ~2.1.2
      // suppress toast by setting prevLocksRef to match before locking
      prevLocksRef.current = { [marcusQ.id]: USERS[0].id, [priyaQ.id]: USERS[1].id };
      setLocks({ [marcusQ.id]: USERS[0].id, [priyaQ.id]: USERS[1].id });
      // All locks start active by default
      setLockStates({ [marcusQ.id]: "active", [priyaQ.id]: "active" });
      // Seed Marcus with the action modal open — this is the demo case
      setOthersModalOpen({ [marcusQ.id]: "q2NoAction" });
      // Seed their in-progress drafts
      setOthersDrafts({
        [marcusQ.id]: {
          ...emptyAnswer(),
          q1: "yes",
          q2: "no",
          q2NoFindings: "Walks happen ad-hoc. No formal cadence",
          q2NoAction: { title: "Establish monthly leadership walk programme", desc: "", owner: "", priority: "med", due: "" },
          lastEditedBy: USERS[0].id,
          savedAt: Date.now() - 18000,
        },
        [priyaQ.id]: {
          ...emptyAnswer(),
          q1: "no",
          q1NoFindings: "Coaching is informal. No template, no recorded outcomes.",
          q1NoAction: null,
          lastEditedBy: USERS[1].id,
          savedAt: Date.now() - 42000,
        },
      });
      // Mark them as "currently typing" so the field-level indicator appears
      setTypingMap({
        [marcusQ.id]: { user: USERS[0], field: "q2NoFindings" },
        [priyaQ.id]:  { user: USERS[1], field: "q1NoFindings" },
      });
    }
  }, [questions, t.conflictModel]);

  // Simulate Marcus continuing to type — appends to his findings every ~3.5s
  useEffect(() => {
    if (t.conflictModel !== "soft-lock") return;
    const tail = " — only 4 of 12 walks completed in last 6 months.";
    let pos = 0;
    const iv = setInterval(() => {
      setOthersDrafts(prev => {
        const next = { ...prev };
        Object.entries(prev).forEach(([qId, draft]) => {
          if (locks[qId] && locks[qId] !== "me") {
            const editingByMarcus = locks[qId] === USERS[0].id;
            if (editingByMarcus && draft.q2NoFindings && pos < tail.length) {
              const add = tail.slice(pos, Math.min(pos + 6, tail.length));
              next[qId] = { ...draft, q2NoFindings: draft.q2NoFindings + add, savedAt: Date.now() };
            }
          }
        });
        return next;
      });
      pos += 6;
      if (pos > tail.length + 24) pos = 0; // loop the typing for the demo
    }, 1800);
    return () => clearInterval(iv);
  }, [t.conflictModel, locks]);

  // Body attributes
  useEffect(() => {
    document.body.setAttribute("data-rail", t.showRail ? "visible" : "hidden");
    document.body.setAttribute("data-nav",  t.showNav  ? "visible" : "hidden");
  }, [t.showRail, t.showNav]);

  // Role badge
  useEffect(() => {
    const el = document.getElementById("role-badge");
    if (!el) return;
    if (t.role === "completer") {
      el.className = "role-badge completer";
      el.querySelector("span").textContent = "Completer · You";
    } else {
      el.className = "role-badge collaborator";
      el.querySelector("span").textContent = "Collaborator · You";
    }
  }, [t.role]);

  // Update answer + autosave
  const updateAnswer = useCallback((id, next) => {
    setAnswers(prev => ({ ...prev, [id]: { ...next, lastEditedBy: "me" } }));
    setSaveStates(prev => ({ ...prev, [id]: "saving" }));
    clearTimeout(timersRef.current[id]);
    timersRef.current[id] = setTimeout(() => {
      setAnswers(prev => ({ ...prev, [id]: { ...prev[id], savedAt: Date.now() } }));
      setSaveStates(prev => ({ ...prev, [id]: "saved" }));
    }, 700);
  }, []);

  useEffect(() => () => Object.values(timersRef.current).forEach(clearTimeout), []);

  // Progress
  const { done, total } = useMemo(() => {
    let d = 0;
    questions.forEach(q => { if (deriveStatus(answers[q.id]) === "done") d += 1; });
    return { done: d, total: questions.length };
  }, [answers, questions]);
  const pct = Math.round((done / total) * 100);

  useEffect(() => {
    document.getElementById("progress-fill").style.width = pct + "%";
    document.getElementById("progress-done").textContent = done;
    document.getElementById("progress-total").textContent = total;
    document.getElementById("progress-pct").textContent = pct + "%";
  }, [done, total, pct]);

  // Aggregate save indicator
  useEffect(() => {
    const states = Object.values(saveStates);
    const anySaving = states.some(s => s === "saving");
    const anySaved  = states.some(s => s === "saved");
    const el = document.getElementById("save-indicator");
    if (!el) return;
    el.classList.toggle("saving", anySaving);
    el.classList.toggle("saved",  !anySaving && anySaved);
    el.querySelector("span").textContent = anySaving ? "Saving…" : (anySaved ? "All saved" : "Not started");
  }, [saveStates]);

  // Render header presence stack via React portal so we can animate joins/leaves.
  // ME is always present. Other users randomly join/leave on a slow cadence —
  // each transition fires the av-presence-enter / av-presence-leave keyframes
  // and a toast that names who came or went.
  const ALL_USERS = useMemo(() => [ME, ...USERS], []);
  const [presentIds, setPresentIds] = useState(() => new Set(ALL_USERS.map(u => u.id)));
  const [enteringIds, setEnteringIds] = useState(() => new Set());
  const [leavingIds, setLeavingIds] = useState(() => new Set());
  const presenceMountedRef = useRef(false);

  const presenceHost = typeof document !== "undefined" ? document.getElementById("presence-stack") : null;

  // Rerender for relative times
  const [, tick] = useState(0);
  useEffect(() => {
    const i = setInterval(() => tick(x => x + 1), 20000);
    return () => clearInterval(i);
  }, []);

  // Activities for rail: simulate based on locks / typing
  const activities = useMemo(() => {
    const acts = {};
    USERS.forEach(u => { acts[u.id] = null; });
    Object.entries(locks).forEach(([qId, uid]) => {
      const q = questions.find(q => q.id === qId);
      if (q && uid !== "me") acts[uid] = { qId, qNum: q.num, qText: q.text, typing: false };
    });
    Object.entries(typingMap).forEach(([qId, { user }]) => {
      const q = questions.find(q => q.id === qId);
      if (q && user.id !== "me") acts[user.id] = { qId, qNum: q.num, qText: q.text, typing: true };
    });
    return acts;
  }, [locks, typingMap, questions]);

  // Toasts
  const [toasts, setToasts] = useState([]);
  const toastIdRef = useRef(1);
  const pushToast = useCallback((toast) => {
    const id = toastIdRef.current++;
    setToasts(prev => [...prev, { id, ...toast }]);
    setTimeout(() => {
      setToasts(prev => prev.map(t => t.id === id ? { ...t, leaving: true } : t));
      setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 250);
    }, toast.duration || 5000);
  }, []);
  const dismissToast = useCallback((id) => {
    setToasts(prev => prev.map(t => t.id === id ? { ...t, leaving: true } : t));
    setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 250);
  }, []);

  // Presence sim — random user joins/leaves the session on a slow cadence,
  // triggering the av-presence-enter / av-presence-leave keyframes in the
  // header and a toast with the user's name.
  useEffect(() => {
    let timeoutId = null;
    const cleanupTimeouts = [];
    const step = () => {
      const candidates = USERS.map(u => u.id);
      if (candidates.length === 0) return;
      const target = candidates[Math.floor(Math.random() * candidates.length)];
      const user = ALL_USERS.find(u => u.id === target);
      setPresentIds(prev => {
        const isPresent = prev.has(target);
        if (isPresent) {
          // LEAVE — animate then remove (silent, no toast)
          setLeavingIds(l => { const n = new Set(l); n.add(target); return n; });
          const t = setTimeout(() => {
            setPresentIds(p => { const n = new Set(p); n.delete(target); return n; });
            setLeavingIds(l => { const n = new Set(l); n.delete(target); return n; });
          }, 320);
          cleanupTimeouts.push(t);
          return prev;
        } else {
          // JOIN — insert and mark entering (silent, no toast)
          setEnteringIds(e => { const n = new Set(e); n.add(target); return n; });
          const t = setTimeout(() => {
            setEnteringIds(e => { const n = new Set(e); n.delete(target); return n; });
          }, 460);
          cleanupTimeouts.push(t);
          const next = new Set(prev); next.add(target); return next;
        }
      });
      timeoutId = setTimeout(step, 14000 + Math.random() * 12000);
    };
    timeoutId = setTimeout(() => { presenceMountedRef.current = true; step(); }, 7000 + Math.random() * 3000);
    return () => {
      if (timeoutId) clearTimeout(timeoutId);
      cleanupTimeouts.forEach(t => clearTimeout(t));
    };
  }, [ALL_USERS, pushToast]);

  // Just-locked / just-jumped row classes (animation triggers)
  const [animatedRows, setAnimatedRows] = useState({}); // {qId: 'just-locked' | 'just-jumped-to'}
  const flashRow = useCallback((qId, kind) => {
    setAnimatedRows(prev => ({ ...prev, [qId]: kind }));
    setTimeout(() => {
      setAnimatedRows(prev => {
        const n = { ...prev };
        delete n[qId];
        return n;
      });
    }, 1700);
  }, []);

  // Watch locks for newly-acquired-by-others — emit toast + pulse the row
  const prevLocksRef = useRef({});
  useEffect(() => {
    const prev = prevLocksRef.current;
    Object.entries(locks).forEach(([qId, uid]) => {
      if (uid === "me") return;
      if (prev[qId] !== uid) {
        const q = questions.find(q => q.id === qId);
        if (!q) return;
        flashRow(qId, "just-locked");
        pushToast({
          kind: "lock",
          user: userById(uid),
          qId,
          qNum: q.num,
          qText: q.text,
          verb: "started editing",
        });
      }
    });
    // Released locks
    Object.entries(prev).forEach(([qId, uid]) => {
      if (uid === "me") return;
      if (!locks[qId]) {
        const q = questions.find(q => q.id === qId);
        if (!q) return;
        pushToast({
          kind: "unlock",
          user: userById(uid),
          qId,
          qNum: q.num,
          qText: q.text,
          verb: "finished editing",
          duration: 3500,
        });
      }
    });
    prevLocksRef.current = { ...locks };
  }, [locks, questions, pushToast, flashRow]);

  // Jump-to-question handler (used by rail clicks and toast jump buttons)
  const jumpToQ = useCallback((qId) => {
    setExpandedId(curr => curr === qId ? curr : null); // collapse current
    // Wait a tick so DOM updates, then scroll & flash
    setTimeout(() => {
      const el = document.getElementById("row-" + qId);
      if (el) {
        window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - 140, behavior: "smooth" });
        flashRow(qId, "just-jumped-to");
      }
    }, 50);
  }, [flashRow]);

  // Header "editing now" chip — count of questions other users are actively in
  useEffect(() => {
    const chip = document.getElementById("editing-now-chip");
    const text = document.getElementById("editing-now-text");
    if (!chip || !text) return;
    const activeQs = new Set();
    Object.entries(locks).forEach(([qId, uid]) => { if (uid !== "me") activeQs.add(qId); });
    Object.keys(typingMap).forEach(qId => {
      if (typingMap[qId].user.id !== "me") activeQs.add(qId);
    });
    const n = activeQs.size;
    chip.hidden = n === 0;
    text.innerHTML = "<b>" + n + "</b> question" + (n === 1 ? "" : "s") + " being edited";
    chip.onclick = () => {
      const first = Array.from(activeQs)[0];
      if (first) jumpToQ(first);
    };
  }, [locks, typingMap, jumpToQ]);

  // Modal shown when the user tries to leave a partial Q. Until they decide
  // (Resume = cancel, Continue = apply), the new Q is NOT locked.
  const [incompleteModal, setIncompleteModal] = useState(null); // {fromQ, toQ} | null

  // Refs so onExpand can read latest values without changing identity each render.
  const expandedIdRef = useRef(expandedId);
  const answersRef = useRef(answers);
  useEffect(() => { expandedIdRef.current = expandedId; }, [expandedId]);
  useEffect(() => { answersRef.current = answers; }, [answers]);

  // Apply an expansion: release old lock, acquire new one, set expanded.
  // Used by onExpand when no confirmation needed, and by the modal's "Continue".
  const applyExpand = useCallback((id) => {
    const prevId = expandedIdRef.current;
    setExpandedId(id);
    if (t.conflictModel === "soft-lock") {
      setLocks(prev => {
        const next = { ...prev };
        if (prevId && prevId !== id && next[prevId] === "me") delete next[prevId];
        if (!next[id] || next[id] === "me") next[id] = "me";
        return next;
      });
    }
  }, [t.conflictModel]);

  // Soft-lock: when I try to expand a Q, check if I was already mid-answer on a
  // different Q in a partial state. If so, show a blocking modal — the new Q
  // is NOT locked until I confirm. Otherwise, apply immediately.
  const onExpand = useCallback((id) => {
    const prevId = expandedIdRef.current;
    if (t.conflictModel === "soft-lock" && prevId && prevId !== id) {
      const prevAnswer = answersRef.current[prevId];
      const prevStatus = prevAnswer ? deriveStatus(prevAnswer) : "empty";
      if (prevStatus === "partial") {
        const prevQ = questions.find(q => q.id === prevId);
        const newQ  = questions.find(q => q.id === id);
        if (prevQ && newQ) {
          setIncompleteModal({ fromQ: prevQ, toQ: newQ });
          return; // gate the navigation until the user decides
        }
      }
    }
    applyExpand(id);
  }, [t.conflictModel, questions, applyExpand]);

  const onCollapse = useCallback(() => {
    if (t.conflictModel === "soft-lock" && expandedId && locks[expandedId] === "me") {
      setLocks(prev => {
        const next = { ...prev };
        delete next[expandedId];
        return next;
      });
    }
    setExpandedId(null);
  }, [t.conflictModel, expandedId, locks]);

  const requestTakeover = useCallback(() => {
    if (!expandedId) return;
    // Simulated: lock transfers to me after a short delay
    setTimeout(() => {
      setLocks(prev => ({ ...prev, [expandedId]: "me" }));
    }, 700);
  }, [expandedId]);

  const dismissConflict = useCallback(() => {
    if (!expandedId) return;
    setConflicts(prev => {
      const n = { ...prev };
      delete n[expandedId];
      return n;
    });
  }, [expandedId]);

  const dismissInvalidation = useCallback(({ accept }) => {
    if (!expandedId) return;
    const c = conflicts[expandedId];
    if (!c) return;
    setAnswers(prev => {
      const a = prev[expandedId];
      if (accept === "mine") {
        // Restore stashed data + revert Q1 to "yes"
        return { ...prev, [expandedId]: { ...a, q1: "yes", q2: "no",
          q2NoFindings: c.stashed.q2NoFindings,
          q2NoAction: c.stashed.q2NoAction,
          lastEditedBy: "me"
        }};
      } else {
        // accept theirs: Q1 stays as the simulated new value; clear stash
        return prev;
      }
    });
    setConflicts(prev => {
      const n = { ...prev };
      delete n[expandedId];
      return n;
    });
  }, [expandedId, conflicts]);

  /* ----- Simulations ----- */
  const sims = useMemo(() => ({
    lockFirstUnanswered: () => {
      // Find the first unanswered Q that's not currently expanded by me
      const target = questions.find(q => deriveStatus(answers[q.id]) === "empty");
      if (!target) return;
      setLocks(prev => ({ ...prev, [target.id]: USERS[0].id }));
    },
    releaseLocks: () => {
      setLocks(prev => {
        const n = { ...prev };
        Object.keys(n).forEach(k => { if (n[k] !== "me") delete n[k]; });
        return n;
      });
      setLockStates({});
      setOthersModalOpen({});
      setTypingMap(prev => {
        const n = { ...prev };
        Object.keys(n).forEach(k => { if (n[k].user.id !== "me") delete n[k]; });
        return n;
      });
    },
    marcusOpenModal: () => {
      // Toggle Marcus's modal-open state on his locked question
      const marcusLockedQ = Object.keys(locks).find(qId => locks[qId] === USERS[0].id);
      if (!marcusLockedQ) {
        alert("Marcus isn't currently locking a question. Try 'Marcus opens an unanswered question' first.");
        return;
      }
      setOthersModalOpen(prev => {
        const n = { ...prev };
        if (n[marcusLockedQ]) delete n[marcusLockedQ];
        else n[marcusLockedQ] = "q2NoAction";
        return n;
      });
    },
    markIdle: () => {
      const marcusLockedQ = Object.keys(locks).find(qId => locks[qId] === USERS[0].id);
      if (!marcusLockedQ) {
        alert("Marcus isn't currently locking a question.");
        return;
      }
      setLockStates(prev => ({ ...prev, [marcusLockedQ]: "idle" }));
      // Stop typing while idle
      setTypingMap(prev => {
        const n = { ...prev };
        delete n[marcusLockedQ];
        return n;
      });
      // Close any modal he had open
      setOthersModalOpen(prev => {
        const n = { ...prev };
        delete n[marcusLockedQ];
        return n;
      });
    },
    marcusJumpsAwayMidEdit: () => {
      // 1) Pick a "from" question Marcus will start editing.
      //    Prefer an unanswered Q that isn't expanded by me and isn't already locked.
      const from = questions.find(q =>
        deriveStatus(answers[q.id]) === "empty" &&
        q.id !== expandedId &&
        !locks[q.id]
      ) || questions.find(q => q.id !== expandedId && !locks[q.id]);
      if (!from) {
        alert("No suitable question to simulate.");
        return;
      }

      // 2) Lock + typing — Marcus is mid-answer.
      setLocks(prev => ({ ...prev, [from.id]: USERS[0].id }));
      setLockStates(prev => ({ ...prev, [from.id]: "active" }));
      setTypingMap(prev => ({ ...prev, [from.id]: { user: USERS[0], field: "q1NoFindings" } }));

      // 3) After ~1.8s, jump: release `from`, lock a fresh `to`, push toast.
      setTimeout(() => {
        const fromIdx = questions.findIndex(q => q.id === from.id);
        const after = questions.slice(fromIdx + 1);
        const to =
          after.find(q => !locks[q.id] && q.id !== expandedId) ||
          questions.find(q => q.id !== from.id && !locks[q.id] && q.id !== expandedId);
        if (!to) return;

        setLocks(prev => {
          const n = { ...prev };
          delete n[from.id];
          n[to.id] = USERS[0].id;
          return n;
        });
        setLockStates(prev => {
          const n = { ...prev };
          delete n[from.id];
          n[to.id] = "active";
          return n;
        });
        setTypingMap(prev => {
          const n = { ...prev };
          delete n[from.id];
          n[to.id] = { user: USERS[0], field: "q1NoFindings" };
          return n;
        });

        pushToast({
          kind: "lock-released",
          user: USERS[0],
          fromQId: from.id, fromQNum: from.num, fromQText: from.text,
          toQId:   to.id,   toQNum:   to.num,   toQText:   to.text,
          duration: 7000,
        });
      }, 1800);
    },
    markDisconnected: () => {
      const marcusLockedQ = Object.keys(locks).find(qId => locks[qId] === USERS[0].id);
      if (!marcusLockedQ) {
        alert("Marcus isn't currently locking a question.");
        return;
      }
      setLockStates(prev => ({ ...prev, [marcusLockedQ]: "disconnected" }));
      setTypingMap(prev => {
        const n = { ...prev };
        delete n[marcusLockedQ];
        return n;
      });
      setOthersModalOpen(prev => {
        const n = { ...prev };
        delete n[marcusLockedQ];
        return n;
      });
      // Toast: Marcus left
      const q = questions.find(q => q.id === marcusLockedQ);
      if (q) {
        pushToast({
          kind: "takeover",
          user: USERS[0],
          qId: marcusLockedQ,
          qNum: q.num,
          qText: q.text,
          verb: "disconnected mid-answer at",
          duration: 8000,
        });
      }
    },
    ynaConflict: () => {
      if (!expandedId) {
        alert("Open a question first, then try this simulation.");
        return;
      }
      const a = answers[expandedId];
      if (a.q1 === null) {
        alert("Set Q1 first so there's a conflict to show.");
        return;
      }
      // Marcus picked a different value
      const options = ["yes", "no", "na"].filter(v => v !== a.q1);
      const theirPick = options[Math.floor(Math.random() * options.length)];
      setConflicts(prev => ({ ...prev, [expandedId]: {
        type: "yna", by: USERS[0].id, field: "q1", myPick: a.q1, theirPick
      }}));
    },
    invalidation: () => {
      if (!expandedId) {
        alert("Open a question first, then try this simulation.");
        return;
      }
      const a = answers[expandedId];
      if (a.q1 !== "yes" || a.q2 !== "no") {
        alert("Set Q1=Yes, Q2=No, and type something in the findings field, then click this.");
        return;
      }
      // Stash current Q2-No work; simulate Marcus setting Q1 to N/A
      const stashed = { q2NoFindings: a.q2NoFindings, q2NoAction: a.q2NoAction };
      setAnswers(prev => ({ ...prev, [expandedId]: {
        ...prev[expandedId],
        q1: "na", q2: null,
        q2NoFindings: "", q2NoAction: null,
        stash: stashed,
        lastEditedBy: USERS[0].id,
      }}));
      setConflicts(prev => ({ ...prev, [expandedId]: {
        type: "invalidation", by: USERS[0].id,
        newQ1: "na", stashed,
      }}));
    },
    startTyping: () => {
      if (!expandedId) return;
      const a = answers[expandedId];
      const field = a.q1 === "no" ? "q1NoFindings"
                   : (a.q1 === "yes" && a.q2 === "no" ? "q2NoFindings" : null);
      if (!field) {
        alert("Open a Q and pick Q1=No or Q1=Yes+Q2=No so a findings field is visible.");
        return;
      }
      setTypingMap(prev => ({ ...prev, [expandedId]: { user: USERS[1], field } }));
    },
    stopTyping: () => {
      setTypingMap({});
    },
    assignSampleQuestions: () => {
      const sample = {};
      questions.slice(0, 10).forEach((q, i) => {
        sample[q.id] = i % 3 === 0 ? "me" : (i % 3 === 1 ? USERS[0].id : USERS[1].id);
      });
      setAssignments(sample);
    },
    clearAssignments: () => setAssignments({}),
  }), [expandedId, answers, questions]);

  // Keep a ref to latest sims so any auto-sim effect doesn't need to re-run
  // every render.
  const simsRef = useRef(sims);
  useEffect(() => { simsRef.current = sims; }, [sims]);

  // Compute per-row "others viewing/editing" presence
  const presenceMap = useMemo(() => {
    const map = {};
    Object.entries(locks).forEach(([qId, uid]) => {
      if (uid !== "me") {
        if (!map[qId]) map[qId] = [];
        map[qId].push(uid);
      }
    });
    Object.entries(typingMap).forEach(([qId, { user }]) => {
      if (user.id !== "me") {
        if (!map[qId]) map[qId] = [];
        if (!map[qId].includes(user.id)) map[qId].push(user.id);
      }
    });
    return map;
  }, [locks, typingMap]);

  /* ===== Render ===== */
  const showSubmit = t.role === "completer";

  // Count of actions across all answers — drives the tab badge.
  const actionsCount = useMemo(() => {
    let n = 0;
    questions.forEach(q => {
      const a = answers[q.id];
      if (a.q1NoAction) n += 1;
      if (a.q2NoAction) n += 1;
    });
    return n;
  }, [questions, answers]);

  // Jump to a question and ensure the Questions tab is active first.
  const jumpToQFromAnywhere = useCallback((qId) => {
    setActiveTab("questions");
    setExpandedId(qId);
    setTimeout(() => jumpToQ(qId), 60);
  }, [jumpToQ]);

  return (
    <>
      <div className="ws-tabs" role="tablist">
        <button type="button" role="tab"
          className={"ws-tab" + (activeTab === "questions" ? " active" : "")}
          aria-selected={activeTab === "questions"}
          onClick={() => setActiveTab("questions")}>
          Questions <span className="count">{total}</span>
        </button>
        <button type="button" role="tab"
          className={"ws-tab" + (activeTab === "actions" ? " active" : "")}
          aria-selected={activeTab === "actions"}
          onClick={() => setActiveTab("actions")}>
          Actions <span className="count">{actionsCount}</span>
        </button>
      </div>

      {activeTab === "actions" ? (
        <ActionsView
          questions={questions}
          answers={answers}
          onOpenAction={(qId, field) => setMyModal({ qId, field })}
          onJumpToQ={jumpToQFromAnywhere}
        />
      ) : (<>
      <div className="ws-list">
        {questions.map(q => (
          <div key={q.id} id={"row-" + q.id} className={animatedRows[q.id] ? "anim-wrap " + animatedRows[q.id] : ""}>
            <MemoQuestionRow
              q={q}
              answer={answers[q.id]}
              setAnswer={(next) => updateAnswer(q.id, next)}
              expanded={expandedId === q.id}
              onExpand={() => onExpand(q.id)}
              onCollapse={onCollapse}
              t={t}
              saveState={saveStates[q.id] || "idle"}
              conflictModel={t.conflictModel}
              role={t.role}
              lockedBy={locks[q.id] || null}
              conflict={conflicts[q.id] || null}
              assignment={assignments[q.id] || null}
              presenceOthers={presenceMap[q.id] || null}
              requestTakeover={requestTakeover}
              dismissConflict={dismissConflict}
              dismissInvalidation={dismissInvalidation}
              assignTo={(uid) => setAssignments(prev => ({ ...prev, [q.id]: uid }))}
              typingByUser={typingMap[q.id] || null}
              flashClass={animatedRows[q.id] || ""}
              lockedDraft={othersDrafts[q.id] || null}
              lockState={lockStates[q.id] || "active"}
              lockerModalField={othersModalOpen[q.id] || null}
              onOpenActionModal={(qId, field) => setMyModal({ qId, field })}
            />
          </div>
        ))}
      </div>

      </>)}

      {/* Incomplete-question modal: gates the navigation. The new Q is NOT
          locked until the user confirms here. */}
      {incompleteModal && (
        <div className="incomplete-modal-overlay" onClick={() => setIncompleteModal(null)}>
          <div className="incomplete-modal" role="dialog" aria-modal="true"
            onClick={(e) => e.stopPropagation()}>
            <div className="incomplete-modal-head">
              <div className="incomplete-modal-ico">{I.flag}</div>
              <button type="button" className="incomplete-modal-close"
                aria-label="Close" onClick={() => setIncompleteModal(null)}>{I.x}</button>
            </div>
            <h3 className="incomplete-modal-h">Leave {incompleteModal.fromQ.num} incomplete?</h3>
            <p className="incomplete-modal-sub">
              <b>{incompleteModal.fromQ.num}</b> still has open sub-questions. If you continue, your partial answers will be discarded and your lock released so anyone on the team can start fresh.
            </p>
            <div className="incomplete-modal-card">
              <div className="incomplete-modal-card-num">{incompleteModal.fromQ.num}</div>
              <div className="incomplete-modal-card-text">{incompleteModal.fromQ.text}</div>
            </div>
            <div className="incomplete-modal-actions">
              <button type="button" className="incomplete-modal-btn secondary"
                onClick={() => {
                  // Resume = stay on the partial Q. Dismiss modal, do nothing.
                  setIncompleteModal(null);
                }}>
                Resume {incompleteModal.fromQ.num}
              </button>
              <button type="button" className="incomplete-modal-btn primary"
                onClick={() => {
                  // Continue = discard partial answers on the abandoned Q,
                  // release its lock, acquire the new one, jump.
                  const fromId = incompleteModal.fromQ.id;
                  const toId = incompleteModal.toQ.id;
                  setIncompleteModal(null);
                  // Reset the abandoned Q to its empty state so nothing
                  // partial is left behind for the next person.
                  setAnswers(prev => ({ ...prev, [fromId]: emptyAnswer() }));
                  applyExpand(toId);
                }}>
                Discard &amp; continue to {incompleteModal.toQ.num}
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Action modal — rendered at App level so it overlays everything */}
      {myModal && (() => {
        const q = questions.find(x => x.id === myModal.qId);
        if (!q) return null;
        const a = answers[myModal.qId];
        const current = a[myModal.field];
        return (
          <ActionModal
            value={current}
            qNum={q.num}
            qText={q.text}
            keyId={"ACT-" + q.id.toUpperCase() + (myModal.field === "q2NoAction" ? "-E" : "")}
            onChange={(next) => updateAnswer(myModal.qId, { ...a, [myModal.field]: next })}
            onDelete={() => {
              updateAnswer(myModal.qId, { ...a, [myModal.field]: null });
              setMyModal(null);
            }}
            onClose={() => setMyModal(null)}
          />
        );
      })()}

      {/* Left question navigator */}
      {ReactDOM.createPortal(
        <Navigator
          questions={questions}
          answers={answers}
          expandedId={expandedId}
          locks={locks}
          lockStates={lockStates}
          onJumpToQ={jumpToQ}
          done={done}
          total={total}
        />,
        document.getElementById("nav-root")
      )}

      {/* Header presence stack (rendered via portal so we can animate joins/leaves) */}
      {presenceHost && ReactDOM.createPortal(
        <>
          {ALL_USERS.filter(u => presentIds.has(u.id) || leavingIds.has(u.id)).map(u => {
            const cls = ["av"];
            if (enteringIds.has(u.id)) cls.push("av-entering");
            if (leavingIds.has(u.id))  cls.push("av-leaving");
            return (
              <span key={u.id} className={cls.join(" ")} title={u.name}>
                <img src={u.avatar} alt={u.name} />
                <span className={"dot " + (u.id === "sarah" ? "dot-idle" : "dot-online")}></span>
              </span>
            );
          })}
        </>,
        presenceHost
      )}

      {/* Right-column session rail (rendered via portal) */}
      {t.showRail && ReactDOM.createPortal(
        <SessionRail activities={activities} conflictModel={t.conflictModel}
          role={t.role} sims={sims} presenceMap={presenceMap}
          onJumpToQ={jumpToQ} questions={questions} />,
        document.getElementById("rail-root")
      )}

      {/* Activity toasts */}
      {ReactDOM.createPortal(
        <>
          {toasts.map(toast => {
            // "Marcus left Q X mid-answer and jumped to Q Y — lock released on X."
            // Two-line, with a Jump button targeting the new (now-locked) Q.
            if (toast.kind === "lock-released") {
              const isMe = toast.user.id === "me";
              return (
                <div key={toast.id} className={"toast lock-released" + (toast.leaving ? " leaving" : "")}>
                  <span className="av"><img src={toast.user.avatar} alt=""/></span>
                  <div className="body">
                    <div>
                      <b>{isMe ? "You" : toast.user.name.split(" ")[0]}</b>{" "}
                      {isMe ? "left" : "left"} <b>{toast.fromQNum}</b> incomplete
                    </div>
                    <div className="meta" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 280 }}>
                      Lock released · {isMe ? "now editing" : "now editing"} <b style={{ color: "var(--text-secondary)" }}>{toast.toQNum}</b>
                    </div>
                  </div>
                  <button type="button" className="jump" onClick={() => { jumpToQ(toast.fromQId); dismissToast(toast.id); }}>
                    {I.eye} {isMe ? "Resume" : "Open"}
                  </button>
                  <button type="button" className="close" aria-label="Dismiss" onClick={() => dismissToast(toast.id)}>{I.x}</button>
                </div>
              );
            }
            return (
              <div key={toast.id} className={"toast " + toast.kind + (toast.leaving ? " leaving" : "")}>
                <span className="av"><img src={toast.user.avatar} alt=""/></span>
                <div className="body">
                  <div>
                    <b>{toast.user.name.split(" ")[0]}</b> {toast.verb} <b>{toast.qNum}</b>
                  </div>
                  <div className="meta" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: 240 }}>
                    {toast.qText}
                  </div>
                </div>
                <button type="button" className="jump" onClick={() => { jumpToQ(toast.qId); dismissToast(toast.id); }}>
                  {I.eye} Jump
                </button>
                <button type="button" className="close" aria-label="Dismiss" onClick={() => dismissToast(toast.id)}>{I.x}</button>
              </div>
            );
          })}
        </>,
        document.getElementById("toast-root")
      )}

      {/* Tweaks panel */}
      <window.TweaksPanel title="Tweaks">
        <window.TweakSection label="Conflict model" />
        <window.TweakRadio
          label="Strategy" value={t.conflictModel}
          options={[
            { value: "soft-lock",  label: "Soft-lock" },
            { value: "optimistic", label: "Optimistic" },
            { value: "assignment", label: "Assigned" },
          ]}
          onChange={v => { setTweak("conflictModel", v); setConflicts({}); setLocks({}); setTypingMap({}); }}
        />

        <window.TweakSection label="Your role" />
        <window.TweakRadio
          label="Role" value={t.role}
          options={[
            { value: "completer",    label: "Completer" },
            { value: "collaborator", label: "Collaborator" },
          ]}
          onChange={v => setTweak("role", v)}
        />

        <window.TweakSection label="Display" />
        <window.TweakToggle label="Left navigator" value={t.showNav}
          onChange={v => setTweak("showNav", v)} />
        <window.TweakToggle label="Session rail" value={t.showRail}
          onChange={v => setTweak("showRail", v)} />
        <window.TweakToggle label="Avatars on rows" value={t.showRowAvatars}
          onChange={v => setTweak("showRowAvatars", v)} />
        <window.TweakToggle label="Typing indicators" value={t.showTypingIndicators}
          onChange={v => setTweak("showTypingIndicators", v)} />

        <window.TweakSection label="Animation" />
        <window.TweakRadio
          label="Reveal" value={t.anim}
          options={[
            { value: "slide", label: "Slide" },
            { value: "snap",  label: "Instant" },
          ]}
          onChange={v => setTweak("anim", v)}
        />
      </window.TweaksPanel>
    </>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
