/* ============================================================
   Project Workspace — Header + Tabs + Structure Screen
   ============================================================ */

/* ---- File icon helper ---- */
const fileIc = t => t==="xlsx" ? "grid" : t==="docx" ? "file-text" : "file";

/* ---- WPID Status badge — 4-state File Lifecycle per requirement v5.0 §4.5 ----
   pending → in-progress → in-review → done
   Backward compat: legacy 6-state keys (waiting-review / rn-* / completed) map in. */
const STATUS_LEGACY_MAP = {
  "waiting-review": "in-review",
  "rn-assigned":    "in-review",
  "rn-responded":   "in-review",
  "rn-rejected":    "in-progress",
  "completed":      "done",
};
const normalizeWpidStatus = (s) => STATUS_LEGACY_MAP[s] || s;

const wpStatus = (s, compact) => {
  const key = normalizeWpidStatus(s);
  const meta = WPID_STATUS_META[key];
  const sz = compact ? 9 : 10;
  if (!meta) return <span className="sbadge" style={{fontSize:10.5}}>{s}</span>;
  return (
    <span className={`sbadge ${meta.tone}`} style={{fontSize:10.5}} title={meta.detail}>
      <Icon n={meta.icon} s={sz}/> {meta.label}
    </span>
  );
};

/* ---- Set Report Date modal — opens the Soft Lock period (γA) ----
   Per req v5.0 §4.12 — once the user fills "วันที่หน้ารายงานผู้สอบบัญชี"
   (= T0), the entire project enters Soft Lock immediately and a 60-day
   countdown to Hard Lock begins. */
const SetReportDateModal = ({ project, requiredSignedReady, onClose, onSave }) => {
  const today = "2026-05-22"; /* DEMO_TODAY-aligned */
  const [date, setDate] = React.useState(today);
  const valid = date && date.length === 10;

  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:520, width:"94vw"}}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--warn-soft)",color:"var(--warn-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="clock" s={16}/>
          </div>
          <h3>Set Report Date (T0)</h3>
        </div>
        <div className="modal-body">
          <div className={`banner ${requiredSignedReady ? "muted" : "danger"}`} style={{marginBottom:14}}>
            <div className="b-ico"><Icon n={requiredSignedReady ? "info" : "alert"} s={15}/></div>
            <div className="b-body">
              {requiredSignedReady ? (
                <>
                  <span className="b-title">Ready to lock</span>
                  All Required slots across every WPID have been signed. Setting T0 will <b>immediately Soft-Lock</b> the entire project (read-only for everyone). The Hard Lock auto-fires <b>60 days after T0</b>.
                </>
              ) : (
                <>
                  <span className="b-title">Blocked — Required sign-offs incomplete</span>
                  T0 cannot be set until <b>every WPID has all its Required roles signed</b> (req §4.12). Finish the outstanding reviews first.
                </>
              )}
            </div>
          </div>

          <div className="field">
            <label>Report Date (T0) <span className="req">*</span></label>
            <input type="date" value={date} onChange={e => setDate(e.target.value)} style={{fontFamily:"var(--mono)"}}/>
            <div className="help">This is the auditor's report date. The 60-day archival window starts from here.</div>
          </div>

          <div className="banner muted" style={{marginTop:14}}>
            <div className="b-ico"><Icon n="lock" s={14}/></div>
            <div className="b-body">
              <span className="b-title">What changes once you save</span>
              <ul style={{margin:"4px 0 0",paddingLeft:18,fontSize:12,lineHeight:1.5}}>
                <li>The entire project becomes <b>read-only</b> (Soft Lock) — no uploads, edits, or WPID changes.</li>
                <li>Members can still <b>view</b> everything as before.</li>
                <li>During Soft Lock: Supervisor (member) or Admin can <b>Archive Now</b>, or Admin can <b>Normal Unlock</b>.</li>
                <li>If nothing is done within 60 days → automatic Hard Lock.</li>
              </ul>
            </div>
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={!valid || !requiredSignedReady} onClick={() => onSave(date)}>
            <Icon n="lock" s={13}/> Set T0 & Soft Lock
          </button>
        </div>
      </div>
    </div>
  );
};

/* ---- Project Header ---- */
const ProjectHead = ({ project, entity, softLocked, hardLocked, signReadiness, onSetReportDate, onZipProject, onContextMenu, onOpenRecycleBin, pinned, onTogglePin, wpStats }) => {
  const meta = PROJECT_META[project.id] || { stage:"Active", prog:50, lead:"—", openNotes:0, due:"—" };

  /* Description: clamp to 2 lines with a more/less toggle that only appears
     when the text actually overflows. */
  const [descExpanded, setDescExpanded] = React.useState(false);
  const [descClamped, setDescClamped]   = React.useState(false);
  const descRef = React.useRef(null);
  React.useLayoutEffect(() => {
    const el = descRef.current;
    if (el && !descExpanded) setDescClamped(el.scrollHeight > el.clientHeight + 1);
  }, [project.description, descExpanded]);

  /* 60-day Hard Lock countdown from auditorReportDate — anchored to the
     demo clock so it matches the Soft Lock banner and Archive screen */
  const daysToLock = (() => {
    if (!project.auditorReportDate) return null;
    const t0 = new Date(project.auditorReportDate);
    const lockDate = new Date(t0.getTime() + 60 * 24 * 60 * 60 * 1000);
    const today = (typeof DEMO_TODAY !== "undefined" && DEMO_TODAY) || new Date();
    return Math.ceil((lockDate - today) / (24 * 60 * 60 * 1000));
  })();


  return (
    <div className="page-head" onContextMenu={onContextMenu}>
      <div style={{display:"flex",alignItems:"flex-start",gap:14,flexWrap:"wrap"}}>
        <div style={{flex:1,minWidth:0}}>
          <div className="page-title-row">
            <h1 className="page-title">{project.name}</h1>
            {onTogglePin && (
              <button className="icon-btn" onClick={onTogglePin}
                title={pinned ? "Unpin from sidebar" : "Pin to sidebar for quick access"}
                style={{width:26,height:26, ...(pinned ? {color:"var(--accent)",background:"var(--accent-soft)",borderColor:"var(--accent-soft)"} : {})}}>
                <Icon n="pin" s={14}/>
              </button>
            )}
            {daysToLock !== null && (
              <span className={`sbadge ${daysToLock <= 7 ? "danger" : daysToLock <= 30 ? "warn" : ""}`}
                style={{fontSize:11.5}}
                title={`auditorReportDate: ${project.auditorReportDate} — Hard Lock in ${daysToLock} days`}>
                <Icon n="clock" s={10}/> T‑{daysToLock} days
              </span>
            )}
          </div>
          {/* Meta line — plain text only (user decision 2026-06-12: badges +
             mono + text mixed three visual styles for what is all just
             reference info). Risk keeps its ink color, no badge chrome;
             tooltips carry the explanations. */}
          <div className="page-sub" style={{display:"flex",alignItems:"center",gap:6,flexWrap:"wrap"}}>
            {project.category && (
              <Tooltip text={CATEGORY_INFO[project.category]?.desc || ""}>
                <span style={{cursor:"help"}}>{CATEGORY_INFO[project.category]?.label || `Category ${project.category}`}</span>
              </Tooltip>
            )}
            {project.riskLevel && (() => {
              const info = RISK_INFO[project.riskLevel] || {};
              const ink = info.tone === "danger" ? "var(--danger-ink)" : info.tone === "warn" ? "var(--warn-ink)" : "var(--ok-ink)";
              return <>
                <span>·</span>
                <Tooltip text={info.desc || ""}>
                  <span style={{color:ink,fontWeight:500}}>
                    {info.label || project.riskLevel} risk
                  </span>
                </Tooltip>
              </>;
            })()}
            <span>·</span>
            <span>
              {project.serviceType || "—"}
              {project.periodStart && project.periodEnd && (
                <> · Period {project.periodStart} → {project.periodEnd}</>
              )}
              {" · "}
              <span title="Cloud storage used by this project">{fmtMB(projectStorageMB(project.id))} used</span>
            </span>
          </div>
          {project.description && (
            <div style={{marginTop:6, maxWidth:760}}>
              <div ref={descRef} style={{
                fontSize:13, lineHeight:1.55, color:"var(--ink-2)", overflowWrap:"anywhere",
                ...(descExpanded ? {} : {
                  display:"-webkit-box", WebkitLineClamp:2, WebkitBoxOrient:"vertical", overflow:"hidden",
                }),
              }}>
                {project.description}
              </div>
              {(descClamped || descExpanded) && (
                <button
                  onClick={()=>setDescExpanded(v=>!v)}
                  style={{marginTop:2, padding:0, background:"none", border:0, color:"var(--muted)", cursor:"pointer", fontSize:12, fontWeight:500}}>
                  {descExpanded ? "Show less" : "… more"}
                </button>
              )}
            </div>
          )}
        </div>
        {/* Actions sit top-right where the progress bar used to be —
           progress was removable noise (user decision 2026-06-12) */}
        <div className="page-actions" style={{display:"flex",gap:6,flexWrap:"wrap",justifyContent:"flex-end"}}>
        <button className="btn sm" onClick={onZipProject} title="Download all folders + files as ZIP">
          <Icon n="download" s={12}/> Download project (.zip)
        </button>
        {/* Guard per req §4.12 — the button is usable ONLY when every WPID
            has all its Required slots signed. Until then it renders disabled
            with a progress counter so the gate is visible in the demo. */}
        {!softLocked && !hardLocked && onSetReportDate && (
          signReadiness?.ready ? (
            <button className="btn sm" onClick={onSetReportDate}
              style={{background:"var(--warn-soft)",color:"var(--warn-ink)",borderColor:"var(--warn-soft)"}}
              title="All Required sign-offs complete — enter the auditor's report date (T0) to start Soft Lock">
              <Icon n="clock" s={12}/> Set Report Date
            </button>
          ) : (
            <Tooltip text={`Locked until every WPID has its Required sign-offs — ${signReadiness?.done ?? 0} of ${signReadiness?.total ?? 0} WPIDs ready (req §4.12)`}>
              <button className="btn sm" disabled style={{opacity:.55,cursor:"not-allowed"}}>
                <Icon n="lock" s={12}/> Set Report Date
                <span className="mono" style={{fontSize:10.5,marginLeft:4}}>{signReadiness?.done ?? 0}/{signReadiness?.total ?? 0}</span>
              </button>
            </Tooltip>
          )
        )}
        </div>
      </div>

      {/* WPID status summary — same count-card row as Users & Roles.
         Replaces the removed header progress bar (user request 2026-06-12). */}
      {wpStats && (
        <div style={{marginTop:18,display:"flex",gap:10,flexWrap:"wrap"}}>
          <div className="cnt" title={WPID_STATUS_META.done.detail}>
            <span className="val">{wpStats.done} <span style={{fontSize:12,color:"var(--muted)"}}>/ {wpStats.total}</span></span>
            <span className="lbl">Done · {wpStats.pct}%</span>
          </div>
          <div className="cnt" title={WPID_STATUS_META["in-progress"].detail}>
            <span className="val">{wpStats.inProgress}</span>
            <span className="lbl">In Progress</span>
          </div>
          <div className="cnt" title={WPID_STATUS_META["in-review"].detail}>
            <span className="val">{wpStats.inReview}</span>
            <span className="lbl">In Review</span>
          </div>
          <div className="cnt" title={WPID_STATUS_META.pending.detail}>
            <span className="val">{wpStats.pending}</span>
            <span className="lbl">Pending</span>
          </div>
        </div>
      )}
    </div>
  );
};

/* ---- Archive view-reason modal (γC) ----
   Per req v5.0 §4.12 — once the project is Hard-Locked, every file open
   forces a reason for the audit log. Internal staff get Preview + Download;
   Regulator gets Preview Only. */
const ArchiveViewReasonModal = ({ wpid, onClose, onConfirm }) => {
  const [reason, setReason] = React.useState("");
  const isGuest = CURRENT_ADMIN.role === "Guest";
  const valid = reason.trim().length >= 5;
  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:520, width:"94vw"}}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--warn-soft)",color:"var(--warn-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="lock" s={16}/>
          </div>
          <h3>Open archived file</h3>
        </div>
        <div className="modal-body">
          <div className="banner muted" style={{marginBottom:14}}>
            <div className="b-ico"><Icon n="info" s={14}/></div>
            <div className="b-body">
              <span className="b-title">{wpid?.name}</span>
              This project is <b>Hard Locked</b>. {isGuest ? "Regulator role — Preview only, no downloads." : "Preview and Download allowed."} Every open is logged with your reason and timestamp.
            </div>
          </div>
          <div className="field" style={{marginBottom:0}}>
            <label>Reason for opening this file <span className="req">*</span></label>
            <textarea
              value={reason} onChange={e => setReason(e.target.value)}
              placeholder="e.g. Regulator request — supplementary disclosure on revenue recognition"
              style={{minHeight:90}}/>
            <div className="help">At least 5 characters. This goes straight into the immutable Activity Log.</div>
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={!valid} onClick={() => onConfirm(reason.trim())}>
            <Icon n="eye" s={13}/> Log reason & open
          </button>
        </div>
      </div>
    </div>
  );
};

/* ---- Hard Lock banner — fired after Archive Now or 60-day auto archive.
   The project is sealed read-only forever; only Admin can revoke. */
const HardLockBanner = ({ project, isAdmin, onUnlock }) => (
  <div style={{
    background:"var(--panel-2)",
    borderBottom:"1px solid var(--line)",
    padding:"10px 24px",
    display:"flex",alignItems:"center",gap:12,flexWrap:"wrap",
  }}>
    <Icon n="archive" s={16}/>
    <div style={{flex:1, minWidth:0}}>
      <div style={{fontWeight:600, fontSize:13, color:"var(--ink)"}}>
        Archived · Hard Lock
        {project.archivedAt && (
          <span className="mono" style={{fontSize:11, fontWeight:500, marginLeft:8, color:"var(--muted)"}}>
            sealed {project.archivedAt.slice(0,10)} · {project.archiveReason || "manual"}
          </span>
        )}
      </div>
      <div style={{fontSize:11.5, color:"var(--ink-2)", marginTop:2}}>
        Read-only. All non-Admin members have been removed; rejoining requires Request Access. Only Admin can revoke this lock.
      </div>
    </div>
    {isAdmin && onUnlock && (
      <button className="btn sm" onClick={onUnlock} title="Revert Hard Lock back to Active (Admin only)">
        <Icon n="refresh" s={12}/> Unlock (Admin)
      </button>
    )}
  </div>
);

/* ---- Soft Lock banner — sits at the very top of the workspace once T0 is
   set. Read-only signal + countdown + the two exit actions. */
const SoftLockBanner = ({ project, daysToLock, canArchiveNow, canNormalUnlock, onArchiveNow, onNormalUnlock }) => (
  <div style={{
    background:"var(--warn-soft)",
    borderBottom:"1px solid var(--line)",
    padding:"10px 24px",
    display:"flex",alignItems:"center",gap:12,flexWrap:"wrap",
  }}>
    <Icon n="lock" s={16} c=""/>
    <div style={{flex:1, minWidth:0}}>
      <div style={{fontWeight:600, fontSize:13, color:"var(--warn-ink)"}}>
        Soft Lock active · Read-only
        <span className="mono" style={{fontSize:11, fontWeight:500, marginLeft:8}}>
          T0 = {project.auditorReportDate} · Hard Lock in <b>T-{daysToLock} days</b>
        </span>
      </div>
      <div style={{fontSize:11.5, color:"var(--ink-2)", marginTop:2}}>
        No uploads, edits, or WPID changes. Members can still view everything. Choose <b>Archive Now</b> to seal early, or <b>Normal Unlock</b> to revert to Active.
      </div>
    </div>
    {canArchiveNow && (
      <button className="btn sm" onClick={onArchiveNow}
        style={{background:"var(--ink)", color:"#fff", borderColor:"var(--ink)"}}>
        <Icon n="archive" s={12}/> Archive Now
      </button>
    )}
    {canNormalUnlock && (
      <button className="btn sm" onClick={onNormalUnlock}>
        <Icon n="refresh" s={12}/> Normal Unlock
      </button>
    )}
  </div>
);

/* ---- Project Tabs ---- */
const ProjectTabs = ({ activeTab, onTab }) => {
  const pending = WPIDS_2200.filter(w => normalizeWpidStatus(w.status) !== "done").length;
  const openNotes = REVIEW_NOTES_2202.filter(n => !n.resolved).length;
  const tabs = [
    { id:"structure", label:"Structure",  icon:"folder",   n: PROJECT_STRUCTURE.reduce((a,f)=>a+f.items,0) },
    { id:"tasks",     label:"Tasks",      icon:"progress", n: openNotes },
    { id:"team",      label:"Team",       icon:"users",    n: PROJECT_TEAM.length },
    { id:"activity",  label:"Activity",   icon:"history" },
    { id:"recycle",   label:"Recycle Bin",icon:"trash" },
    { id:"settings",  label:"Settings",   icon:"settings" },
  ];
  return (
    <div className="tabbar">
      {tabs.map(t => (
        <button key={t.id}
          className={"tab " + (activeTab===t.id ? "active" : "")}
          onClick={()=>onTab(t.id)}>
          <Icon n={t.icon} s={13}/> {t.label}
          {t.n != null && (
            <span className={"badge-count " + (activeTab===t.id?"active":"")}>
              {t.n}
            </span>
          )}
        </button>
      ))}
    </div>
  );
};

/* ---- Timestamp Override Modal — MTG4 §2.3 ----
   Admin / Supervisor can adjust sign-off timestamp for flexibility. */
/* Confirm before Submit → Waiting for Review — shows exactly who will be
   notified (the WPID's reviewer slots) so the submit isn't a surprise. */
const SubmitReviewModal = ({ wp, chain, onClose, onConfirm }) => {
  const reviewers = (chain || []).filter(c => c.role !== "Preparer");
  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:460}}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--panel-2)",color:"var(--ink)",display:"grid",placeItems:"center"}}>
            <Icon n="eye" s={16}/>
          </div>
          <h3>Submit for review?</h3>
        </div>
        <div className="modal-body">
          <div className="field">
            <label>WPID</label>
            <div className="dval"><b>{wp.name}</b></div>
          </div>
          <div className="field" style={{marginBottom:0}}>
            <label>Reviewers to be notified</label>
            <div style={{display:"flex",flexDirection:"column",gap:6,marginTop:4}}>
              {reviewers.map((c, i) => (
                <div key={i} style={{display:"flex",alignItems:"center",gap:8,fontSize:12.5}}>
                  {c.name
                    ? <Avatar id={c.userId} name={c.name} size="sm"/>
                    : <span style={{width:22,height:22,borderRadius:"50%",border:"1px dashed var(--line-2)",display:"inline-block"}}/>}
                  <span style={{flex:1,minWidth:0}} className="truncate">
                    {c.name || <span style={{color:"var(--muted-2)"}}>Unassigned</span>}
                  </span>
                  <span className="mono" style={{fontSize:10.5,color:"var(--muted)"}}>
                    {c.role}{c.required ? "" : " · optional"}
                  </span>
                </div>
              ))}
            </div>
            <div className="help" style={{marginTop:10}}>
              Status moves to <b>Waiting for Review</b> and everyone above gets a notification. You can pull it back with "← Back to In Progress".
            </div>
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" onClick={onConfirm}>
            <Icon n="send" s={13}/> Submit &amp; notify reviewers
          </button>
        </div>
      </div>
    </div>
  );
};

/* Confirm before restoring an old version — append-only, so the modal
   explains exactly what will happen rather than warning of data loss. */
const RestoreVersionModal = ({ wp, entry, onClose, onConfirm }) => {
  const fromV = entry.v.replace(" (current)", "");
  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:440}}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--panel-2)",color:"var(--ink)",display:"grid",placeItems:"center"}}>
            <Icon n="refresh" s={16}/>
          </div>
          <h3>Restore {fromV}?</h3>
        </div>
        <div className="modal-body">
          <div className="field">
            <label>WPID</label>
            <div className="dval"><b>{wp.name}</b></div>
          </div>
          <div className="field" style={{marginBottom:0}}>
            <label>Version to restore</label>
            <div className="dval">
              <span className="mono" style={{fontWeight:600}}>{fromV}</span>
              <span className="mono muted" style={{fontSize:11}}> · {entry.t}</span>
              {entry.note && <div style={{fontSize:12,color:"var(--ink-2)",marginTop:3}}>{entry.note}</div>}
            </div>
            <div className="help" style={{marginTop:10,lineHeight:1.5}}>
              A <b>new version</b> will be created as a copy of {fromV} — nothing is deleted
              and the full history stays intact. Current sign-offs are marked <b>stale</b>,
              and the restore is recorded in the Activity Log.
            </div>
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" onClick={onConfirm}>
            <Icon n="refresh" s={13}/> Restore version
          </button>
        </div>
      </div>
    </div>
  );
};

/* Owner returns to a draft whose base version is older than current —
   someone uploaded meanwhile. Nothing can be lost either way: continuing
   just appends a newer version, the uploaded one stays in history. */
const StaleDraftModal = ({ wp, onClose, onContinue, onDiscard }) => (
  <div className="modal-veil" onClick={onClose}>
    <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:460}}>
      <div className="modal-head">
        <div style={{width:32,height:32,borderRadius:8,background:"var(--warn-soft)",color:"var(--warn-ink)",display:"grid",placeItems:"center"}}>
          <Icon n="alert-triangle" s={16}/>
        </div>
        <h3>The file moved on while you were away</h3>
      </div>
      <div className="modal-body">
        <div className="field">
          <label>WPID</label>
          <div className="dval"><b>{wp.name}</b></div>
        </div>
        <div className="field" style={{marginBottom:0}}>
          <label>What happened</label>
          <div style={{fontSize:12.5,lineHeight:1.6,color:"var(--ink-2)"}}>
            Your draft is based on <span className="mono"><b>{wp.draftBaseV}</b></span>, but the current
            version is now <span className="mono"><b>{wp.v}</b></span> (someone uploaded while your draft
            was open). Nothing is lost either way — saving your draft later just creates a newer
            version on top; <span className="mono">{wp.v}</span> stays in the history.
          </div>
        </div>
      </div>
      <div className="modal-foot">
        <button className="btn danger" onClick={onDiscard}>
          <Icon n="trash" s={13}/> Discard my draft
        </button>
        <button className="btn primary" onClick={onContinue}>
          <Icon n="edit-3" s={13}/> Continue editing
        </button>
      </div>
    </div>
  </div>
);

const TimestampOverrideModal = ({ entry, onClose, onSave }) => {
  const initial = entry.signedAtIso || "2026-05-22T10:00";
  const [ts, setTs] = React.useState(initial);
  const valid = !!ts;

  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:480}}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--warn-soft)",color:"var(--warn-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="edit-3" s={16}/>
          </div>
          <h3>Override sign-off timestamp</h3>
        </div>
        <div className="modal-body">
          <div className="banner warn" style={{marginBottom:14}}>
            <div className="b-ico"><Icon n="alert-triangle" s={14}/></div>
            <div className="b-body">
              <span className="b-title">Admin / Supervisor only</span>
              Overrides are logged automatically. Use when flexibility is genuinely needed (e.g. late entry of an offline sign-off).
            </div>
          </div>

          <div className="field">
            <label>Sign-off role</label>
            <div className="dval" style={{display:"flex",alignItems:"center",gap:8}}>
              <b>{entry.role}</b>
              {entry.name && <span className="muted">· {entry.name}</span>}
            </div>
          </div>
          <div className="field" style={{marginBottom:0}}>
            <label>New timestamp <span className="req">*</span></label>
            <input type="datetime-local" value={ts} onChange={e=>setTs(e.target.value)} style={{fontFamily:"var(--mono)"}} autoFocus/>
            <div className="help">Original: <span className="mono">{entry.signedAt || "—"}</span></div>
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={!valid} onClick={()=>onSave({ts})}>
            <Icon n="check" s={13}/> Save override
          </button>
        </div>
      </div>
    </div>
  );
};

/* ---- Sign-Chain Configuration Modal — MTG4 §2.2 ----
   Admin / Supervisor can toggle Required ↔ Optional for each review role
   per project. */
const SignChainConfigModal = ({ chain, onClose, onSave }) => {
  const [config, setConfig] = React.useState(chain.map(c => ({...c})));
  const toggle = (idx) => setConfig(prev => prev.map((c,i) => i === idx ? {...c, required: !c.required} : c));
  const requiredCount = config.filter(c => c.required).length;

  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:520}}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--accent-soft)",color:"var(--accent-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="check" s={16}/>
          </div>
          <h3>Configure sign-off chain</h3>
        </div>
        <div className="modal-body">
          <div className="banner muted" style={{marginBottom:14}}>
            <div className="b-ico"><Icon n="info" s={14}/></div>
            <div className="b-body">
              Toggle each review role between <b>Required</b> (must sign before WPID completes) and <b>Optional</b> (can sign but not blocking).
              {" "}<b>{requiredCount}</b> required · {config.length - requiredCount} optional.
            </div>
          </div>
          <div style={{display:"flex",flexDirection:"column",gap:8}}>
            {config.map((c, i) => (
              <div key={i} style={{
                display:"flex",alignItems:"center",gap:10,
                padding:"10px 12px",
                border:"1px solid var(--line)",borderRadius:"var(--radius)",
                background: c.required ? "var(--warn-soft)" : "var(--panel-3)",
              }}>
                <div style={{flex:1}}>
                  <div style={{fontWeight:600}}>{c.role}</div>
                  <div className="muted" style={{fontSize:11.5}}>{c.name || "Not assigned"}</div>
                </div>
                <div className="seg" style={{display:"flex"}}>
                  <button className={c.required ? "active" : ""} onClick={()=>!c.required && toggle(i)}
                    style={{padding:"5px 10px",fontSize:11.5,border:"1px solid var(--line)",borderRadius:"6px 0 0 6px",background:c.required?"var(--ink)":"#fff",color:c.required?"#fff":"var(--ink)",cursor:"pointer"}}>
                    Required
                  </button>
                  <button className={!c.required ? "active" : ""} onClick={()=>c.required && toggle(i)}
                    style={{padding:"5px 10px",fontSize:11.5,border:"1px solid var(--line)",borderLeft:"none",borderRadius:"0 6px 6px 0",background:!c.required?"var(--ink)":"#fff",color:!c.required?"#fff":"var(--ink)",cursor:"pointer"}}>
                    Optional
                  </button>
                </div>
              </div>
            ))}
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" onClick={()=>onSave(config)}>
            <Icon n="check" s={13}/> Save configuration
          </button>
        </div>
      </div>
    </div>
  );
};

/* ---- Reviewer Slots Panel — per req v5.0 §4.6 ----
   Each slot = one seat held by one reviewer; the panel renders the slots
   defined on the project, with each slot's own sub-state (pending review /
   signed). Sign-off NO longer enforces order — any pending slot signs
   independently. Required slots must all sign before WPID becomes Done;
   Optional slots can sign but don't block. */
const ReviewerSlotsPanel = ({ chain, activeVersion, postT0, onSign, onCancel, onConfigure, onOverrideTimestamp, onAddSlot, onOpenSheet }) => {
  const statusIcon = (s) => {
    if (s==="signed")   return <span style={{width:18,height:18,borderRadius:"50%",background:"var(--ok-soft)",color:"var(--ok-ink)",display:"grid",placeItems:"center",flexShrink:0}}><Icon n="check" s={10} w={2.5}/></span>;
    if (s==="pending")  return <span style={{width:18,height:18,borderRadius:"50%",background:"var(--warn-soft)",color:"var(--warn-ink)",display:"grid",placeItems:"center",flexShrink:0}}><Icon n="clock" s={10}/></span>;
    if (s==="skipped")  return <span style={{width:18,height:18,borderRadius:"50%",background:"var(--panel-2)",color:"var(--muted)",display:"grid",placeItems:"center",flexShrink:0}}><Icon n="x" s={9}/></span>;
    if (s==="optional") return <span style={{width:18,height:18,borderRadius:"50%",background:"var(--panel-2)",color:"var(--muted)",display:"grid",placeItems:"center",flexShrink:0}}><span style={{fontSize:8,fontWeight:600,fontFamily:"var(--mono)"}}>opt</span></span>;
    return null;
  };

  const allRequiredDone = chain.length > 0 && chain.every(c => !c.required || c.status === "signed");
  const canOverride = CURRENT_ADMIN.role === "Admin" || CURRENT_ADMIN.role === "Supervisor";

  return (
    <div>
      <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:8,gap:6}}>
        <div className="mono" style={{fontSize:10,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".06em"}}>Reviewer Roles</div>
        {onConfigure && (CURRENT_ADMIN.role === "Admin" || CURRENT_ADMIN.role === "Supervisor") && (
          <button className="btn ghost sm" onClick={onConfigure} title="Toggle Required / Optional per role" style={{padding:"2px 6px"}}>
            <Icon n="settings" s={11}/> Configure
          </button>
        )}
        <span className="mono" style={{fontSize:10,color:"var(--muted)"}}>Current: {activeVersion}</span>
      </div>

      <div style={{display:"flex",flexDirection:"column",gap:6}}>
        {chain.map((c, i) => {
          const isCurrentSigner = c.status === "signed" && c.userId === CURRENT_ADMIN.id;
          const canCancel       = isCurrentSigner && !postT0;
          return (
            <div key={i} style={{
              display:"flex",alignItems:"center",gap:8,
              padding:"7px 10px",
              border:"1px solid var(--line)",
              borderRadius:"var(--radius)",
              background: c.status==="pending" ? "var(--warn-soft)" : c.status==="signed" ? "var(--ok-soft)" : "var(--panel-3)",
              opacity: c.status==="skipped" || c.status==="optional" ? 0.6 : 1,
            }}>
              {statusIcon(c.status)}
              <div style={{flex:1,minWidth:0,cursor: onOpenSheet ? "pointer" : "default"}} onClick={() => onOpenSheet && onOpenSheet(i)}>
                <div style={{display:"flex",alignItems:"center",gap:6}}>
                  <span style={{fontSize:11.5,fontWeight:600}}>{c.role}</span>
                  {!c.required && <span style={{fontSize:9,color:"var(--muted)",background:"var(--panel-2)",padding:"1px 4px",borderRadius:3,fontFamily:"var(--mono)"}}>optional</span>}
                  {c.qaCount > 0 && <span style={{fontSize:9,color:"var(--warn-ink)",background:"var(--warn-soft)",padding:"1px 4px",borderRadius:3,fontFamily:"var(--mono)"}}>{c.qaCount} review{c.qaCount!==1?"s":""}</span>}
                </div>
                {c.name ? (
                  <div style={{fontSize:11,color:"var(--ink-2)",marginTop:1}}>
                    {c.name}
                    {c.status==="signed" && c.version && (
                      <span className="mono" style={{fontSize:10,color: c.stale ? "var(--warn-ink)" : "var(--muted)",marginLeft:6}}>
                        {c.stale ? `⚠ signed ${c.version} (file updated)` : `✓ ${c.version} · ${c.signedAt}`}
                      </span>
                    )}
                  </div>
                ) : (
                  <div style={{fontSize:11,color:"var(--muted)",fontStyle:"italic",marginTop:1}}>
                    {c.note || (c.status==="pending" ? "Awaiting signature" : "Not assigned")}
                  </div>
                )}
              </div>
              {c.status==="signed" && c.initials && (
                <div style={{display:"flex",alignItems:"center",gap:4,flexShrink:0}}>
                  <div style={{width:22,height:22,borderRadius:"50%",background:"var(--ink)",color:"#fff",display:"grid",placeItems:"center",fontFamily:"var(--mono)",fontSize:8,fontWeight:600}}>
                    {c.initials}
                  </div>
                  {canOverride && onOverrideTimestamp && (
                    <button className="btn ghost sm" onClick={()=>onOverrideTimestamp(i)}
                      title="Override sign-off timestamp (Admin/Supervisor only)" style={{padding:"2px 4px"}}>
                      <Icon n="edit-3" s={10}/>
                    </button>
                  )}
                  {canCancel && onCancel && (
                    <button className="btn ghost sm" onClick={()=>onCancel(i)}
                      title="Cancel your sign-off — only you can cancel, only before T0"
                      style={{padding:"2px 6px",color:"var(--danger-ink)"}}>
                      <Icon n="x" s={11}/> Cancel
                    </button>
                  )}
                </div>
              )}
              {/* No Sign button here — this panel only DISPLAYS signed/unsigned
                 state (signing happens through the review flow). Signed rows
                 keep the timestamp-override pencil. */}
            </div>
          );
        })}
      </div>

      {onAddSlot && (
        <button onClick={onAddSlot}
          style={{marginTop:8,padding:"7px 10px",width:"100%",border:"1px dashed var(--line)",borderRadius:"var(--radius)",background:"transparent",color:"var(--muted)",fontSize:11.5,cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",gap:6}}
          title="Add an extra reviewer role to this WPID — Required will reopen Done WPIDs for review; Optional won't.">
          <Icon n="plus" s={11}/> Add role to this WPID
        </button>
      )}

      {allRequiredDone && (
        <div style={{marginTop:10,padding:"8px 10px",background:"var(--ok-soft)",color:"var(--ok-ink)",borderRadius:"var(--radius)",fontSize:11.5,display:"flex",alignItems:"center",gap:6}}>
          <Icon n="check-circle" s={12}/>
          <span>All Required roles signed — WPID can be marked <b>Done</b>.</span>
        </div>
      )}
    </div>
  );
};

/* ---- Review Sheets Panel — per req v5.0 §4.6 + §4.9 ----
   Replaces the old single-thread Review Notes / Task. Each Reviewer Slot
   on this WPID owns one Sheet that holds review/response rounds.
   (Terminology per user decision 2026-06-12: the workflow is review ↔
   rework until the work passes — so "review/response", not "Q&A".)

   Rules:
   - Reviewer (current slot person) submits 1 review at a time and must
     wait for a response before submitting again.
   - Anyone in the project can respond (system records who replied).
   - When the reviewer is satisfied they Sign-off from the slot panel
     above — sheet closes.
*/
const ReviewSheetsPanel = ({ chain, sheetsBySlot, expandedSlot, setExpandedSlot,
                            onAskQuestion, onAnswerQuestion }) => {
  /* lightweight rich-text renderer — bold / italic / code / line breaks */
  const renderRich = (text) => {
    if (!text) return null;
    const html = text
      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
      .replace(/\*\*(.+?)\*\*/g, "<b>$1</b>")
      .replace(/\*(.+?)\*/g, "<i>$1</i>")
      .replace(/`(.+?)`/g, "<code style='background:var(--panel-2);padding:1px 4px;border-radius:3px;font-size:11px'>$1</code>")
      .replace(/\n/g, "<br/>");
    return <span dangerouslySetInnerHTML={{__html: html}}/>;
  };

  const totalRounds = Object.values(sheetsBySlot || {}).reduce((sum, list) => sum + list.length, 0);
  const openRounds  = Object.values(sheetsBySlot || {}).reduce((sum, list) => sum + list.filter(r => !r.a).length, 0);

  return (
    <div>
      <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:10}}>
        <div className="mono" style={{fontSize:10,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".06em"}}>
          Review Sheets
        </div>
        <div style={{display:"flex",gap:6}}>
          {openRounds > 0   && <span className="sbadge warn" style={{fontSize:10}}>{openRounds} awaiting response</span>}
          {totalRounds > 0  && <span className="sbadge" style={{fontSize:10}}>{totalRounds} round{totalRounds!==1?"s":""}</span>}
        </div>
      </div>

      {chain.length === 0 ? (
        <div className="mono" style={{fontSize:11,color:"var(--muted-2)"}}>No reviewer roles on this WPID.</div>
      ) : (
        <div style={{display:"flex",flexDirection:"column",gap:6}}>
          {chain.map((slot, slotIdx) => {
            const rounds      = (sheetsBySlot || {})[slotIdx] || [];
            const isExpanded  = expandedSlot === slotIdx;
            const lastRound   = rounds[rounds.length - 1];
            const awaitingAns = lastRound && !lastRound.a;
            const isAssigned  = slot.userId === CURRENT_ADMIN.id;

            return (
              <div key={slotIdx} style={{
                border:"1px solid var(--line)",
                borderRadius:"var(--radius)",
                background:"#fff",
                overflow:"hidden",
              }}>
                {/* Sheet header — clickable to expand */}
                <button
                  onClick={() => setExpandedSlot(isExpanded ? null : slotIdx)}
                  style={{
                    width:"100%",display:"flex",alignItems:"center",gap:8,
                    padding:"8px 10px",background:"transparent",border:"none",cursor:"pointer",
                    textAlign:"left",
                  }}>
                  <Icon n={isExpanded ? "chev-d" : "chev-r"} s={11} c="muted"/>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{display:"flex",alignItems:"center",gap:6}}>
                      <span style={{fontSize:11.5,fontWeight:600}}>{slot.role}</span>
                      {!slot.required && <span style={{fontSize:9,color:"var(--muted)",background:"var(--panel-2)",padding:"1px 4px",borderRadius:3,fontFamily:"var(--mono)"}}>optional</span>}
                      {slot.status === "signed" && <span style={{fontSize:9,color:"var(--ok-ink)",background:"var(--ok-soft)",padding:"1px 5px",borderRadius:10,fontFamily:"var(--mono)"}}>✓ signed</span>}
                    </div>
                    <div style={{fontSize:11,color:"var(--ink-2)",marginTop:1}}>
                      {slot.name || <i style={{color:"var(--muted)"}}>Unassigned</i>}
                      {rounds.length > 0 && <span className="mono" style={{marginLeft:8,fontSize:10,color: awaitingAns ? "var(--warn-ink)" : "var(--muted)"}}>
                        · {rounds.length} round{rounds.length!==1?"s":""}{awaitingAns ? " · awaiting response" : ""}
                      </span>}
                    </div>
                  </div>
                </button>

                {/* Sheet body — review/response rounds */}
                {isExpanded && (
                  <div style={{padding:"4px 10px 10px",borderTop:"1px dashed var(--line)"}}>
                    {rounds.length === 0 ? (
                      <div className="mono" style={{fontSize:11,color:"var(--muted-2)",padding:"6px 0"}}>
                        No reviews yet on this sheet.
                      </div>
                    ) : (
                      <div style={{display:"flex",flexDirection:"column",gap:10,padding:"8px 0"}}>
                        {rounds.map((r, ri) => (
                          <div key={r.id || ri} style={{padding:8,background:"var(--panel-3)",borderRadius:"var(--radius)",border:"1px solid var(--line)"}}>
                            <div style={{fontSize:10,color:"var(--muted)",marginBottom:4,fontFamily:"var(--mono)"}}>Round {ri+1}</div>
                            <div style={{fontSize:11.5,lineHeight:1.5}}>
                              <div style={{display:"flex",alignItems:"baseline",gap:6,marginBottom:3}}>
                                <span style={{fontWeight:600,color:"var(--warn-ink)"}}>Review:</span>
                                <span style={{fontSize:10,color:"var(--muted)"}}>{r.qByName} · {r.qAt}</span>
                              </div>
                              <div style={{color:"var(--ink-2)",marginBottom:8,paddingLeft:14}}>{renderRich(r.q)}</div>
                              {r.a ? (
                                <>
                                  <div style={{display:"flex",alignItems:"baseline",gap:6,marginBottom:3}}>
                                    <span style={{fontWeight:600,color:"var(--ok-ink)"}}>Response:</span>
                                    <span style={{fontSize:10,color:"var(--muted)"}}>{r.aByName} · {r.aAt}</span>
                                  </div>
                                  <div style={{color:"var(--ink-2)",paddingLeft:14}}>{renderRich(r.a)}</div>
                                </>
                              ) : (
                                <button className="btn sm primary" style={{marginLeft:14}}
                                  onClick={()=>onAnswerQuestion && onAnswerQuestion(slotIdx, ri)}>
                                  <Icon n="send" s={11}/> Respond to this review
                                </button>
                              )}
                            </div>
                          </div>
                        ))}
                      </div>
                    )}

                    {/* Submit new review — only the slot's assignee, only when nothing awaits a response */}
                    {!awaitingAns && slot.status !== "signed" && (
                      <button className="btn sm" style={{width:"100%",justifyContent:"center",marginTop:4}}
                        disabled={!isAssigned}
                        title={isAssigned ? "" : "Only the reviewer assigned to this role can submit a review"}
                        onClick={()=>onAskQuestion && onAskQuestion(slotIdx)}>
                        <Icon n="edit-3" s={11}/> Submit new review
                      </button>
                    )}
                    {awaitingAns && (
                      <div className="mono" style={{fontSize:10.5,color:"var(--muted)",marginTop:4,textAlign:"center"}}>
                        Waiting for a response before the next review can be submitted.
                      </div>
                    )}
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
};

/* ---- Review Notes / Task Panel ----
   1 WPID = 1 Task — timeline of notes, replies, and resolves  */
const ReviewNotesPanel = ({ notes, onAdd, onResolve }) => {
  const open = notes.filter(n => !n.resolved);
  const resolved = notes.filter(n => n.resolved);

  const noteIcon = (type) => {
    if (type==="note")    return <span style={{width:20,height:20,borderRadius:"50%",background:"var(--warn-soft)",color:"var(--warn-ink)",display:"grid",placeItems:"center",flexShrink:0}}><Icon n="edit-3" s={10}/></span>;
    if (type==="reply")   return <span style={{width:20,height:20,borderRadius:"50%",background:"var(--panel-2)",color:"var(--ink-2)",display:"grid",placeItems:"center",flexShrink:0}}><Icon n="arrow-r" s={10}/></span>;
    if (type==="resolve") return <span style={{width:20,height:20,borderRadius:"50%",background:"var(--ok-soft)",color:"var(--ok-ink)",display:"grid",placeItems:"center",flexShrink:0}}><Icon n="check" s={10}/></span>;
    return null;
  };

  /* Render markdown-lite (bold, italic, code, bullets) */
  const renderRich = (text) => {
    if (!text) return null;
    const html = text
      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
      .replace(/\*\*(.+?)\*\*/g, "<b>$1</b>")
      .replace(/\*(.+?)\*/g, "<i>$1</i>")
      .replace(/`(.+?)`/g, "<code style='background:var(--panel-2);padding:1px 4px;border-radius:3px;font-size:11px'>$1</code>")
      .replace(/^- (.+)$/gm, "<li style='margin-left:14px'>$1</li>")
      .replace(/\n/g, "<br/>");
    return <div style={{fontSize:12,color:"var(--ink-2)",lineHeight:1.5}} dangerouslySetInnerHTML={{__html: html}}/>;
  };

  const renderNote = (n) => (
    <div key={n.id} style={{display:"flex",gap:8,paddingBottom:10}}>
      {noteIcon(n.type)}
      <div style={{flex:1,minWidth:0}}>
        <div style={{display:"flex",alignItems:"center",gap:6,marginBottom:2,flexWrap:"wrap"}}>
          <span style={{fontSize:11.5,fontWeight:500}}>{n.name}</span>
          <span className="mono" style={{fontSize:10,color:"var(--muted)"}}>{n.ts}</span>
          {n.resolved && <span style={{fontSize:9,color:"var(--ok-ink)",background:"var(--ok-soft)",padding:"1px 5px",borderRadius:10,fontFamily:"var(--mono)"}}>resolved</span>}
          {n.assigneeName && n.type === "note" && (
            <span className="badge" style={{fontSize:9.5}} title="Assignee">→ {n.assigneeName}</span>
          )}
        </div>
        {n.subject && (
          <div style={{fontWeight:600,fontSize:12,marginBottom:3}}>{n.subject}</div>
        )}
        {renderRich(n.text)}
        {n.screenshots && n.screenshots.length > 0 && (
          <div style={{marginTop:6,display:"flex",gap:6,flexWrap:"wrap"}}>
            {n.screenshots.map((s, i) => (
              <div key={i} style={{border:"1px solid var(--line)",borderRadius:"var(--radius)",padding:3,background:"#fff",maxWidth:160}}>
                {s.dataUrl ? (
                  <img src={s.dataUrl} alt={s.name} style={{maxWidth:150,maxHeight:90,display:"block",borderRadius:3}}/>
                ) : (
                  <div style={{width:150,height:90,background:"var(--panel-2)",display:"grid",placeItems:"center",color:"var(--muted)",borderRadius:3}}>
                    <span style={{fontSize:10,fontFamily:"var(--mono)"}}>📷 {s.name}</span>
                  </div>
                )}
              </div>
            ))}
          </div>
        )}
        {n.wpidHistory && n.wpidHistory.length > 1 && (
          <details style={{marginTop:6,fontSize:11}}>
            <summary style={{cursor:"pointer",color:"var(--muted)"}}>
              <Icon n="history" s={10}/> WPID reassignment history ({n.wpidHistory.length})
            </summary>
            <div style={{marginTop:4,paddingLeft:14,fontFamily:"var(--mono)",fontSize:10.5,color:"var(--muted-2)",lineHeight:1.6}}>
              {n.wpidHistory.map((h, i) => (
                <div key={i}>WP-{h.wpid} · {h.at}{h.note ? ` · ${h.note}` : ""}</div>
              ))}
            </div>
          </details>
        )}
        {!n.resolved && n.type === "note" && onResolve && (
          <button className="btn ghost sm" style={{marginTop:6,padding:"2px 8px",fontSize:10.5}} onClick={()=>onResolve(n.id)}>
            <Icon n="check" s={10}/> Mark resolved
          </button>
        )}
      </div>
    </div>
  );

  return (
    <div>
      <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:10}}>
        <div className="mono" style={{fontSize:10,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".06em"}}>
          Review Notes
        </div>
        <div style={{display:"flex",gap:6}}>
          {open.length > 0 && <span className="sbadge warn" style={{fontSize:10}}>{open.length} open</span>}
          {resolved.length > 0 && <span className="sbadge ok" style={{fontSize:10}}>{resolved.length} resolved</span>}
        </div>
      </div>

      {notes.length === 0 ? (
        <div className="mono" style={{fontSize:11,color:"var(--muted-2)"}}>No review notes yet.</div>
      ) : (
        <div style={{display:"flex",flexDirection:"column"}}>
          {notes.map(renderNote)}
        </div>
      )}

      <button className="btn sm" style={{width:"100%",justifyContent:"center",marginTop:6}} onClick={onAdd}>
        <Icon n="edit-3" s={11}/> Add Review Note
      </button>
    </div>
  );
};

/* ====== Kanban Board (Task View) — per Function List §7.3 ====== */
const KanbanBoard = ({ wpidsByFolder, notesByWpid, folderTree, onOpenWpid }) => {
  /* Build folder id → display name map */
  const folderNameOf = React.useMemo(() => {
    const map = {};
    (folderTree || []).forEach(f => {
      map[f.id] = f.name;
      if (f.children) f.children.forEach(c => { map[c.id] = c.name; });
    });
    return map;
  }, [folderTree]);

  /* Flatten all WPIDs with folder context */
  const all = React.useMemo(() => {
    return Object.entries(wpidsByFolder || {}).flatMap(([fid, list]) =>
      (list || []).map(w => ({ ...w, folderId: fid, folderName: folderNameOf[fid] || fid }))
    );
  }, [wpidsByFolder, folderNameOf]);

  const [ownerFilter, setOwnerFilter] = React.useState("all");

  const owners = React.useMemo(() => {
    const set = new Set(all.map(w => w.owner).filter(Boolean));
    return [...set];
  }, [all]);

  const filtered = ownerFilter === "all" ? all : all.filter(w => w.owner === ownerFilter);

  /* Kanban board — Pending column hidden (decluttered); pending WPIDs still appear in the Structure list + status filter */
  const columns = WPID_STATUS_KEYS.filter(k => k !== "pending").map(k => ({
    id: k,
    label: WPID_STATUS_META[k].label,
    icon: WPID_STATUS_META[k].icon,
    cls: WPID_STATUS_META[k].tone,
  }));

  const grouped = Object.fromEntries(columns.map(c => [c.id, filtered.filter(w => normalizeWpidStatus(w.status) === c.id)]));

  return (
    <div className="page-body">
      <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:14,justifyContent:"flex-end"}}>
        <span className="mono" style={{fontSize:10.5,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".05em"}}>Owner</span>
        <select className="btn sm" value={ownerFilter} onChange={e=>setOwnerFilter(e.target.value)} style={{paddingRight:24}}>
          <option value="all">All owners</option>
          {owners.map(o => <option key={o} value={o}>{o}</option>)}
        </select>
      </div>

      <div className="kanban" style={{gridTemplateColumns:`repeat(${columns.length}, minmax(0, 1fr))`}}>
        {columns.map(col => {
          const items = grouped[col.id] || [];
          return (
            <div key={col.id} className="kanban-col">
              <div className={"kanban-col-head " + col.cls}>
                <Icon n={col.icon} s={13}/>
                <span style={{fontWeight:600,fontSize:12.5}}>{col.label}</span>
                <span className="mono" style={{fontSize:11,marginLeft:"auto"}}>{items.length}</span>
              </div>
              <div className="kanban-col-body">
                {items.length === 0 ? (
                  <div className="kanban-empty">No WPIDs</div>
                ) : items.map(w => {
                  const openNotes = (notesByWpid[w.id] || []).filter(n => !n.resolved).length;
                  return (
                    <div key={w.id} className="kanban-card" onClick={()=>onOpenWpid(w.folderId, w.id)}>
                      <div className="kanban-card-head">
                        <Icon n={fileIc(w.type)} s={12} c="muted"/>
                        <span className="mono" style={{fontSize:10,color:"var(--muted)"}}>WP-{w.id}</span>
                        <span style={{flex:1}}/>
                        {w.v !== "—" && <span className="mono" style={{fontSize:10,color:"var(--muted-2)"}}>{w.v}</span>}
                      </div>
                      <div className="kanban-card-name">{w.name}</div>
                      <div className="kanban-card-meta">
                        <span className="kanban-folder truncate">{w.folderName}</span>
                      </div>
                      <div className="kanban-card-foot">
                        <span className="kanban-owner mono">{w.owner}</span>
                        <span style={{flex:1}}/>
                        {openNotes > 0 && (
                          <span className="kanban-chip warn" title={`${openNotes} open review note${openNotes>1?"s":""}`}>
                            <Icon n="edit-3" s={9}/> {openNotes}
                          </span>
                        )}
                        {w.updated && w.updated !== "—" && (
                          <span className="mono" style={{fontSize:10,color:"var(--muted-2)"}}>{w.updated}</span>
                        )}
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

/* ====== Inline modals — Preview / Upload / Add Note / Add WPID / Delete WPID ====== */

const PreviewModal = ({ wpid, folderLabel, onClose, onEditOnWeb }) => {
  const targetRef = React.useRef(null);
  const fname = wpid.name.toLowerCase().replace(/\s+/g,"_") + "_" + (wpid.v === "—" ? "v1" : wpid.v) + "." + wpid.type;
  const ext = "." + wpid.type;
  const lock = lockInfo(wpid, CURRENT_ADMIN.id);
  const editable = isWebEditable(wpid.type);

  /* Pick source — prefer uploaded blob, then explicit previewUrl, then sample fallback by type */
  const source = wpid.uploadedFile
    || wpid.previewUrl
    || (typeof SAMPLE_BY_TYPE !== "undefined" ? SAMPLE_BY_TYPE[wpid.type] : null);

  const method = (window.FilePreview ? window.FilePreview.methodOf(ext) : "Unknown");

  React.useEffect(() => {
    if (!targetRef.current) return;
    if (!source) {
      targetRef.current.innerHTML = `<div class="fp-placeholder" style="min-height:200px"><div style="font-size:28px;opacity:.4">📄</div><div style="margin-top:8px">No file available for preview</div></div>`;
      return;
    }
    if (!window.FilePreview) {
      targetRef.current.innerHTML = `<div class="fp-error">FilePreview library not loaded</div>`;
      return;
    }
    window.FilePreview.render(targetRef.current, { source, ext, name: fname });
  }, [source, ext, fname]);

  /* Allow re-download of the source for blob/url */
  const handleDownload = () => {
    if (!source) return;
    const a = document.createElement("a");
    if (source instanceof Blob) {
      a.href = URL.createObjectURL(source);
    } else if (typeof source === "string") {
      a.href = source;
    }
    a.download = fname;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
  };

  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal lg" onClick={e=>e.stopPropagation()} style={{maxWidth:880,width:"94vw"}}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--accent-soft)",color:"var(--accent-ink)",display:"grid",placeItems:"center"}}>
            <Icon n={fileIc(wpid.type)} s={16}/>
          </div>
          <div style={{flex:1,minWidth:0}}>
            <h3 style={{margin:0,fontSize:14}}>{wpid.name}</h3>
            <div className="mono muted" style={{fontSize:10.5,marginTop:2}}>{fname} · {folderLabel}</div>
          </div>
          <span className="badge accent" style={{whiteSpace:"nowrap",fontSize:10.5}} title="Render engine">{method}</span>
          <button className="icon-btn" onClick={onClose}><Icon n="x" s={16}/></button>
        </div>
        <div className="modal-body" style={{maxHeight:"68vh",overflowY:"auto"}}>
          {lock.locked && !lock.isMe && (
            <div className="banner warn" style={{marginBottom:14}}>
              <div className="b-ico"><Icon n="lock" s={14}/></div>
              <div className="b-body">
                <span className="b-title">Locked by {lock.byName}</span>
                Someone is editing this file on the web right now. You can preview but not upload a new version until they're done.
              </div>
            </div>
          )}
          <div className="banner muted" style={{marginBottom:14}}>
            <div className="b-ico"><Icon n="eye" s={14}/></div>
            <div className="b-body">
              <b>Preview-only</b> · Files render in-browser via PDF.js / SheetJS / docx-preview. No file leaves the browser; download requires explicit permission.
            </div>
          </div>
          <div ref={targetRef} style={{minHeight:300}}/>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Close</button>
          <button className="btn" disabled={!source} onClick={handleDownload}><Icon n="download" s={13}/> Download</button>
          {editable && onEditOnWeb && (
            <button className="btn accent"
              disabled={lock.locked && !lock.isMe}
              title={lock.locked && !lock.isMe ? `Locked by ${lock.byName}` : "Open Web-based editor (Word/Excel)"}
              onClick={() => { onEditOnWeb(wpid); }}>
              <Icon n="edit-3" s={13}/> {lock.isMe ? "Continue editing on web" : "Edit on web"}
            </button>
          )}
        </div>
      </div>
    </div>
  );
};

/* =================================================================
   Web Editor (ONLYOFFICE) — Phase 1 POC
   =================================================================
   Per REQ วิเคราะห์การทำ Web Editor.md (2026-06-09 decision):
     - ONLYOFFICE Document Server (Docker on :8080) hosts the iframe
     - Express backend (:3000) serves the doc + receives save callbacks
     - JWT signed by backend; ONLYOFFICE validates with the shared secret

   If the ONLYOFFICE container OR the backend isn't running, we fall
   back to a textarea mock so the rest of the prototype keeps working.
   ================================================================== */

const WEB_EDITOR_BACKEND = "http://localhost:3000";

const WebEditorModal = ({ wpid, folderLabel, onClose, onSave }) => {
  const lock = lockInfo(wpid, CURRENT_ADMIN.id);
  const editorRef = React.useRef(null);    /* DocsAPI instance */
  const placeholderId = React.useMemo(() => `oo-editor-${wpid.id}-${Math.random().toString(36).slice(2,7)}`, [wpid.id]);

  /* phase: connecting → ready → fallback → error */
  const [phase, setPhase]   = React.useState("connecting");
  const [errMsg, setErrMsg] = React.useState("");
  const [editorMode, setEditorMode] = React.useState(null);     /* "onlyoffice" | "mock" */

  /* Mock-fallback state (used when ONLYOFFICE / backend unavailable) */
  const [content, setContent] = React.useState(
    wpid.webEditContent
    || `# ${wpid.name}\n\nVersion: ${wpid.v === "—" ? "v1 (new)" : wpid.v}\nOpened by ${CURRENT_ADMIN.name} on the web.\n\n— Type your edits below (mock mode) —\n\n`
  );
  const [dirty, setDirty] = React.useState(false);

  /* === Bring up ONLYOFFICE editor on mount === */
  React.useEffect(() => {
    let cancelled = false;

    (async () => {
      /* 1. Detect: is api.js loaded? (script in index.html may have failed) */
      if (typeof window.DocsAPI === "undefined") {
        setEditorMode("mock");
        setPhase("fallback");
        setErrMsg("ONLYOFFICE Document Server not detected at :8080. Falling back to the mock editor.");
        return;
      }

      /* 2. Ask backend for a signed editor config */
      let cfg;
      try {
        const r = await fetch(`${WEB_EDITOR_BACKEND}/api/editor-token`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            fileId:   wpid.id,
            fileName: `${wpid.name}.${wpid.type}`,
            userId:   CURRENT_ADMIN.id,
            userName: CURRENT_ADMIN.name,
            mode:     "edit",
          }),
        });
        cfg = await r.json();
        if (!r.ok) throw new Error(cfg.error || `backend ${r.status}`);
      } catch (err) {
        if (cancelled) return;
        setEditorMode("mock");
        setPhase("fallback");
        setErrMsg(`Backend on :3000 not reachable (${err.message}). Falling back to the mock editor.`);
        return;
      }

      if (cancelled) return;

      /* 3. Wait for the placeholder element to actually exist in the DOM
         (React may not have committed the JSX yet when this effect runs). */
      let target = document.getElementById(placeholderId);
      let waits = 0;
      while (!target && waits < 20) {
        await new Promise(r => setTimeout(r, 50));
        target = document.getElementById(placeholderId);
        waits++;
      }
      if (cancelled) return;
      if (!target) {
        setEditorMode("mock");
        setPhase("fallback");
        setErrMsg("Editor placeholder not found in DOM — falling back to mock.");
        return;
      }

      /* 4. Mark mode early so the placeholder div stays mounted, then attach editor */
      setEditorMode("onlyoffice");

      try {
        editorRef.current = new window.DocsAPI.DocEditor(placeholderId, {
          ...cfg,
          width:  "100%",
          height: "100%",
          events: {
            onAppReady: () => setPhase("ready"),
            onDocumentStateChange: (_ev) => { /* _ev.data === true means dirty */ },
            onError: (ev) => {
              console.error("[ONLYOFFICE]", ev);
              setErrMsg(`Editor error: ${ev?.data?.errorDescription || "unknown"}`);
            },
            onRequestClose: () => onClose(),
          },
        });
      } catch (err) {
        if (cancelled) return;
        setEditorMode("mock");
        setPhase("fallback");
        setErrMsg(`Could not mount ONLYOFFICE iframe (${err.message}). Falling back to the mock editor.`);
      }
    })();

    return () => {
      cancelled = true;
      if (editorRef.current && editorRef.current.destroyEditor) {
        try { editorRef.current.destroyEditor(); } catch {}
        editorRef.current = null;
      }
    };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const handleMockChange = (v) => { setContent(v); setDirty(true); };
  const handleMockSave   = () => onSave(content);
  const handleCloseEditor = () => {
    /* In ONLYOFFICE mode, the iframe autosaves via callback — closing the
       modal is enough. The backend snapshots+writes on its own. */
    if (editorMode === "onlyoffice") onSave(null);
    else onClose();
  };

  const docType = (wpid.type === "xlsx" || wpid.type === "xlsm" || wpid.type === "xls") ? "Excel"
                : (wpid.type === "pptx") ? "PowerPoint"
                : "Word";

  return (
    <div className="modal-veil" onClick={editorMode === "onlyoffice" ? undefined : (dirty ? undefined : onClose)}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:1280,width:"96vw",height:"92vh",display:"flex",flexDirection:"column"}}>
        <div className="modal-head" style={{flexShrink:0}}>
          <div style={{width:32,height:32,borderRadius:8,background:"var(--accent-soft)",color:"var(--accent-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="edit-3" s={16}/>
          </div>
          <div style={{flex:1,minWidth:0}}>
            <h3 style={{margin:0,fontSize:14}}>Edit on web — {wpid.name}</h3>
            <div className="mono muted" style={{fontSize:10.5,marginTop:2}}>
              .{wpid.type} · {folderLabel} · {docType} editor
              {editorMode === "onlyoffice" && <span style={{color:"var(--ok-ink)"}}> · ONLYOFFICE (real)</span>}
              {editorMode === "mock"       && <span style={{color:"var(--warn-ink)"}}> · POC mock</span>}
            </div>
          </div>
          {editorMode === "onlyoffice" && (
            <span className="sbadge ok" style={{fontSize:10.5}} title="Powered by ONLYOFFICE Document Server">
              <Icon n="check-circle" s={10}/> ONLYOFFICE
            </span>
          )}
          <span className="sbadge warn" style={{fontSize:10.5}}><Icon n="lock" s={10}/> Locked to you</span>
          <button className="icon-btn" onClick={handleCloseEditor} title="Close (autosave fires on close)"><Icon n="x" s={16}/></button>
        </div>

        <div className="modal-body" style={{flex:1,overflow:"hidden",display:"flex",flexDirection:"column",padding:0,position:"relative"}}>

          {/* ── ONLYOFFICE placeholder — MUST always be in DOM so DocsAPI can attach ── */}
          {/* It's invisible while in fallback mode, but stays mounted to avoid the
              "Could not find DOM element" race when the editor initializes. */}
          <div style={{
            flex: editorMode === "mock" ? 0 : 1,
            position:"relative",
            display: editorMode === "mock" ? "none" : "block",
          }}>
            <div id={placeholderId} style={{position:"absolute",inset:0}}/>

            {/* Spinner overlay — covers the placeholder until onAppReady fires */}
            {phase === "connecting" && (
              <div style={{
                position:"absolute",inset:0,zIndex:10,
                background:"var(--panel-3)",display:"grid",placeItems:"center",
              }}>
                <div style={{textAlign:"center"}}>
                  <div style={{
                    width:36,height:36,borderRadius:"50%",
                    border:"3px solid var(--line)",borderTopColor:"var(--ink)",
                    margin:"0 auto 12px",animation:"spin 1s linear infinite",
                  }}/>
                  <div style={{fontSize:13,fontWeight:500,marginBottom:4}}>Connecting to ONLYOFFICE…</div>
                  <div className="mono muted" style={{fontSize:11}}>:8080 (document server) · :3000 (backend)</div>
                  <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
                </div>
              </div>
            )}
          </div>

          {/* ── mock fallback ────────────────────────────────── */}
          {editorMode === "mock" && (
            <div style={{flex:1,overflow:"auto",padding:14}}>
              <div className="banner warn" style={{marginBottom:14}}>
                <div className="b-ico"><Icon n="alert" s={14}/></div>
                <div className="b-body">
                  <span className="b-title">Mock editor — ONLYOFFICE not reachable</span>
                  {errMsg}
                  <div className="help" style={{marginTop:6}}>
                    To enable the real editor: <span className="mono">docker compose up -d</span> at the prototype root, then <span className="mono">cd backend &amp;&amp; npm install &amp;&amp; npm run seed &amp;&amp; npm start</span>.
                  </div>
                </div>
              </div>

              <div className="banner info" style={{marginBottom:14}}>
                <div className="b-ico"><Icon n="info" s={14}/></div>
                <div className="b-body">
                  <span className="b-title">Web-based editing — replaces Smart Sync</span>
                  Edits save back to the vault. While this editor is open, others see a <b>Locked by {CURRENT_ADMIN.name}</b> indicator and cannot upload over your work.
                </div>
              </div>

              <div style={{display:"flex",gap:4,padding:6,background:"var(--panel-2)",border:"1px solid var(--line)",borderRadius:"var(--radius) var(--radius) 0 0",borderBottom:"none"}}>
                <button className="btn ghost sm" disabled style={{padding:"3px 7px",fontWeight:700}}>B</button>
                <button className="btn ghost sm" disabled style={{padding:"3px 7px",fontStyle:"italic"}}>I</button>
                <button className="btn ghost sm" disabled style={{padding:"3px 7px",textDecoration:"underline"}}>U</button>
                <span style={{width:1,background:"var(--line)",margin:"0 4px"}}/>
                <button className="btn ghost sm" disabled style={{padding:"3px 7px"}}><Icon n="layers" s={11}/> Heading</button>
                <button className="btn ghost sm" disabled style={{padding:"3px 7px"}}><Icon n="grid" s={11}/> Table</button>
                <span style={{flex:1}}/>
                <span className="mono" style={{fontSize:10,color:"var(--muted)",padding:"4px 6px"}}>
                  Autosave: {dirty ? <span style={{color:"var(--warn-ink)"}}>unsaved</span> : <span style={{color:"var(--ok-ink)"}}>up to date</span>}
                </span>
              </div>
              <textarea
                value={content}
                onChange={(e) => handleMockChange(e.target.value)}
                style={{
                  width:"100%",minHeight:380,
                  border:"1px solid var(--line)",borderRadius:"0 0 var(--radius) var(--radius)",
                  borderTop:"none",padding:"14px 16px",
                  fontFamily:"var(--mono)",fontSize:12.5,lineHeight:1.55,
                  resize:"vertical",
                }}
              />
            </div>
          )}
        </div>

        <div className="modal-foot" style={{flexShrink:0}}>
          {editorMode === "onlyoffice" ? (
            <>
              <span className="mono muted" style={{fontSize:11}}>
                Autosave on · Versions capped at 3 (current + 2 previous) per requirement
              </span>
              <span style={{flex:1}}/>
              <button className="btn primary" onClick={handleCloseEditor}>
                <Icon n="check" s={13}/> Close &amp; release lock
              </button>
            </>
          ) : (
            <>
              <button className="btn" onClick={onClose}>{dirty ? "Discard & close" : "Close"}</button>
              <span style={{flex:1}}/>
              <button className="btn primary" onClick={handleMockSave}>
                <Icon n="check" s={13}/> Save &amp; release lock
              </button>
            </>
          )}
        </div>
      </div>
    </div>
  );
};

const UploadModal = ({ wpid, folderLabel, onClose, onConfirm }) => {
  const lock = lockInfo(wpid, CURRENT_ADMIN.id);
  const locked = lock.locked && !lock.isMe;
  const [phase, setPhase] = React.useState(locked ? "locked" : "pick"); // locked | pick | uploading | done
  const [pct, setPct] = React.useState(0);
  const [note, setNote] = React.useState("");
  const [pickedFile, setPickedFile] = React.useState(null);
  const inputRef = React.useRef(null);

  React.useEffect(() => {
    if (phase !== "uploading") return;
    let p = 0;
    const id = setInterval(() => {
      p += Math.random() * 18 + 6;
      if (p >= 100) { p = 100; clearInterval(id); setTimeout(()=>setPhase("done"), 200); }
      setPct(Math.round(p));
    }, 160);
    return () => clearInterval(id);
  }, [phase]);

  const start = () => { setPct(0); setPhase("uploading"); };
  const finish = () => onConfirm({ note: note.trim(), file: pickedFile });

  const expectedFname = wpid.name.toLowerCase().replace(/\s+/g,"_") + "." + wpid.type;
  const newV = wpid.v === "—" ? "v1" : "v" + (parseInt(wpid.v.replace(/\D/g,""), 10) + 1);

  const onPick = (e) => {
    const f = e.target.files && e.target.files[0];
    if (f) setPickedFile(f);
  };

  const onDrop = (e) => {
    e.preventDefault();
    const f = e.dataTransfer.files && e.dataTransfer.files[0];
    if (f) setPickedFile(f);
  };

  const formatSize = (bytes) => {
    if (bytes < 1024) return bytes + " B";
    if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + " KB";
    return (bytes/1048576).toFixed(1) + " MB";
  };

  return (
    <div className="modal-veil" onClick={phase==="uploading"?undefined:onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--accent-soft)",color:"var(--accent-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="upload" s={16}/>
          </div>
          <h3>Upload to {wpid.name}</h3>
        </div>
        <div className="modal-body">
          {phase === "locked" && (
            <div className="banner warn" style={{marginBottom:0}}>
              <div className="b-ico"><Icon n="lock" s={14}/></div>
              <div className="b-body">
                <span className="b-title">File is locked by {lock.byName}</span>
                Someone is editing this file on the web right now. Upload is blocked to prevent overwriting their work. Try again once their editor closes.
              </div>
            </div>
          )}
          {phase === "pick" && (
            <>
              <div className="banner muted" style={{marginBottom:14}}>
                <div className="b-ico"><Icon n="info" s={14}/></div>
                <div className="b-body">
                  Expected file type: <b>.{wpid.type}</b>. Uploading creates version <b className="mono">{newV}</b> · previous 2 versions retained.
                </div>
              </div>
              <input
                ref={inputRef}
                type="file"
                accept={"." + wpid.type}
                style={{display:"none"}}
                onChange={onPick}
              />
              <div
                onClick={()=>inputRef.current && inputRef.current.click()}
                onDragOver={e=>{e.preventDefault();}}
                onDrop={onDrop}
                style={{border:"2px dashed var(--line)",borderRadius:"var(--radius-lg)",padding:"24px 16px",textAlign:"center",background:"var(--panel-3)",cursor:"pointer"}}
              >
                <Icon n="upload" s={28} c="muted"/>
                <div style={{marginTop:10,fontSize:13,fontWeight:500}}>
                  {pickedFile ? "Replace file" : "Drop a file here or click to browse"}
                </div>
                <div className="muted" style={{fontSize:11,marginTop:4}}>Accepts <span className="mono">.{wpid.type}</span></div>
                <div className="mono" style={{fontSize:10.5,color:"var(--muted)",marginTop:14,padding:"6px 10px",background:"#fff",border:"1px solid var(--line)",borderRadius:"var(--radius)",display:"inline-block"}}>
                  {pickedFile ? `${pickedFile.name} · ${formatSize(pickedFile.size)}` : `(no file picked yet — sample placeholder: ${expectedFname})`}
                </div>
              </div>
              <div className="field" style={{marginTop:14,marginBottom:0}}>
                <label>Version note</label>
                <textarea value={note} onChange={e=>setNote(e.target.value)} placeholder="Brief description of what changed in this version…" style={{minHeight:64}}/>
              </div>
            </>
          )}
          {phase === "uploading" && (
            <div style={{padding:"12px 0"}}>
              <div className="mono" style={{fontSize:11,color:"var(--muted)",marginBottom:8}}>
                Uploading {pickedFile ? pickedFile.name : expectedFname}…
              </div>
              <div style={{height:8,background:"var(--panel-2)",borderRadius:4,overflow:"hidden",marginBottom:10}}>
                <div style={{height:"100%",background:"var(--ink)",width:pct+"%",borderRadius:4,transition:"width .12s ease"}}/>
              </div>
              <div className="mono" style={{fontSize:11,textAlign:"right",color:"var(--ink-2)"}}>{pct}%</div>
              <div className="muted" style={{fontSize:11,marginTop:12,lineHeight:1.5}}>
                Computing SHA-256 hash · Anchoring to chain · Encrypting at rest …
              </div>
            </div>
          )}
          {phase === "done" && (
            <div style={{padding:"4px 0"}}>
              <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:14}}>
                <span style={{width:32,height:32,borderRadius:"50%",background:"var(--ok-soft)",color:"var(--ok-ink)",display:"grid",placeItems:"center",flexShrink:0}}>
                  <Icon n="check" s={16}/>
                </span>
                <div>
                  <div style={{fontWeight:600}}>Upload complete</div>
                  <div className="mono muted" style={{fontSize:11,marginTop:2}}>
                    {pickedFile ? `${pickedFile.name} · ${newV} · ${formatSize(pickedFile.size)}` : `${expectedFname} · ${newV}`}
                  </div>
                </div>
              </div>
              <div className="banner muted">
                <div className="b-ico"><Icon n="info" s={14}/></div>
                <div className="b-body">
                  Status moved to <b>In Progress</b>. Prior signers keep signatures but a "file updated" notice appears on their row.
                </div>
              </div>
            </div>
          )}
        </div>
        <div className="modal-foot">
          {phase === "locked" && (
            <button className="btn" onClick={onClose}>Close</button>
          )}
          {phase === "pick" && (
            <>
              <button className="btn" onClick={onClose}>Cancel</button>
              <button className="btn primary" onClick={start}><Icon n="upload" s={13}/> Upload</button>
            </>
          )}
          {phase === "uploading" && (
            <button className="btn" disabled>Uploading…</button>
          )}
          {phase === "done" && (
            <button className="btn primary" onClick={finish}><Icon n="check" s={13}/> Done</button>
          )}
        </div>
      </div>
    </div>
  );
};

/* Enhanced AddNoteModal per MTG4 §2.4 — subject, assignee, rich text, image paste */
const AddNoteModal = ({ wpid, allWpids, onClose, onSave }) => {
  const [subject, setSubject] = React.useState("");
  const [text, setText] = React.useState("");
  const [assigneeId, setAssigneeId] = React.useState(wpid.preparedBy || PROJECT_TEAM[0]?.id || "");
  const [targetWpid, setTargetWpid] = React.useState(wpid.id);
  const [screenshots, setScreenshots] = React.useState([]);
  const textRef = React.useRef(null);
  const valid = subject.trim().length >= 3 && text.trim().length >= 5 && assigneeId;

  /* === Rich-text-style toolbar — wraps selection with markdown ===
     Pure POC inline formatting since we want to avoid dragging in a heavy editor lib. */
  const applyMd = (left, right=null) => {
    const ta = textRef.current; if (!ta) return;
    const before = text.slice(0, ta.selectionStart);
    const sel    = text.slice(ta.selectionStart, ta.selectionEnd);
    const after  = text.slice(ta.selectionEnd);
    const wrapped = sel.length > 0 ? sel : "text";
    const r = right == null ? left : right;
    setText(before + left + wrapped + r + after);
    setTimeout(()=>{ ta.focus(); ta.setSelectionRange(before.length + left.length, before.length + left.length + wrapped.length); }, 0);
  };

  /* === Image paste handler — captures clipboard images === */
  const onPaste = (e) => {
    const items = e.clipboardData && e.clipboardData.items;
    if (!items) return;
    for (const item of items) {
      if (item.type && item.type.startsWith("image/")) {
        e.preventDefault();
        const blob = item.getAsFile();
        const reader = new FileReader();
        reader.onload = (ev) => {
          const dataUrl = ev.target.result;
          /* lightweight measure */
          const img = new Image();
          img.onload = () => {
            setScreenshots(prev => [...prev, {
              name: `pasted-image-${prev.length+1}.png`,
              dataUrl, w: img.width, h: img.height,
            }]);
          };
          img.src = dataUrl;
        };
        reader.readAsDataURL(blob);
      }
    }
  };

  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:640, width:"94vw"}}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--warn-soft)",color:"var(--warn-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="edit-3" s={16}/>
          </div>
          <h3>New Review Note</h3>
        </div>
        <div className="modal-body">
          <div className="banner muted" style={{marginBottom:14}}>
            <div className="b-ico"><Icon n="info" s={14}/></div>
            <div className="b-body">
              Every Review Note has subject, target WPID, assignee, and rich-text description. WPID can be reassigned later (with history). The WPID cannot move to Completed until all open notes are resolved.
            </div>
          </div>

          <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:14}}>
            <div className="field">
              <label>Target WPID <span className="req">*</span></label>
              <select value={targetWpid} onChange={e=>setTargetWpid(e.target.value)}>
                {(allWpids || [wpid]).map(w => (
                  <option key={w.id} value={w.id}>WP-{w.id} · {w.name}</option>
                ))}
              </select>
            </div>
            <div className="field">
              <label>Assignee <span className="req">*</span></label>
              <select value={assigneeId} onChange={e=>setAssigneeId(e.target.value)}>
                {PROJECT_TEAM.map(m => (
                  <option key={m.id} value={m.id}>{m.name} ({m.role})</option>
                ))}
              </select>
            </div>
          </div>

          <div className="field">
            <label>Subject <span className="req">*</span></label>
            <input type="text" value={subject} onChange={e=>setSubject(e.target.value)}
              placeholder="Short title — e.g. Materiality benchmark check"
              autoFocus/>
          </div>

          <div className="field" style={{marginBottom:0}}>
            <label>Description / notes <span className="req">*</span></label>
            <div style={{display:"flex",gap:4,padding:6,background:"var(--panel-2)",border:"1px solid var(--line)",borderRadius:"var(--radius) var(--radius) 0 0",borderBottom:"none"}}>
              <button type="button" className="btn ghost sm" onClick={()=>applyMd("**","**")} style={{padding:"3px 7px",fontWeight:700}} title="Bold (Ctrl+B)">B</button>
              <button type="button" className="btn ghost sm" onClick={()=>applyMd("*","*")} style={{padding:"3px 7px",fontStyle:"italic"}} title="Italic">I</button>
              <button type="button" className="btn ghost sm" onClick={()=>applyMd("`","`")} style={{padding:"3px 7px",fontFamily:"var(--mono)"}} title="Code">{"<>"}</button>
              <button type="button" className="btn ghost sm" onClick={()=>applyMd("- ", "")} style={{padding:"3px 7px"}} title="Bullet">•</button>
              <span style={{flex:1}}/>
              <span className="mono muted" style={{fontSize:10,padding:"4px 6px"}}>
                <Icon n="info" s={10}/> Paste a screenshot (Ctrl+V) to attach
              </span>
            </div>
            <textarea
              ref={textRef}
              value={text}
              onChange={e=>setText(e.target.value)}
              onPaste={onPaste}
              placeholder="Be specific — explain what to revise and why.&#10;&#10;Tip: paste a screenshot directly (Ctrl+V), or use **bold** and *italic* markdown."
              style={{minHeight:140, fontFamily:"var(--mono)", fontSize:12.5, borderRadius:"0 0 var(--radius) var(--radius)", borderTop:"none"}}/>

            {screenshots.length > 0 && (
              <div style={{marginTop:8,display:"flex",gap:6,flexWrap:"wrap"}}>
                {screenshots.map((s, i) => (
                  <div key={i} style={{position:"relative",border:"1px solid var(--line)",borderRadius:"var(--radius)",padding:4,background:"#fff"}}>
                    <img src={s.dataUrl} alt={s.name} style={{maxWidth:140,maxHeight:90,display:"block",borderRadius:4}}/>
                    <button type="button" onClick={()=>setScreenshots(prev=>prev.filter((_,j)=>j!==i))}
                      style={{position:"absolute",top:-6,right:-6,width:18,height:18,borderRadius:"50%",background:"var(--ink)",color:"#fff",border:"none",cursor:"pointer",fontSize:11}}>×</button>
                    <div className="mono" style={{fontSize:9,color:"var(--muted)",marginTop:3,textAlign:"center",maxWidth:140,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{s.name}</div>
                  </div>
                ))}
              </div>
            )}
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={!valid} onClick={()=>onSave({
            subject: subject.trim(), text: text.trim(), assigneeId, targetWpid, screenshots,
          })}>
            <Icon n="send" s={13}/> Post note
          </button>
        </div>
      </div>
    </div>
  );
};

/* Delete project — two-step confirm (GitHub-style):
   1) download a ZIP backup first, 2) type the client's Tax ID to unlock delete. */
const DeleteProjectModal = ({ project, entity, onClose, onZip, onConfirm }) => {
  const [zipped, setZipped] = React.useState(false);
  const [taxInput, setTaxInput] = React.useState("");
  const match = taxInput.trim() === entity.taxId;
  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:520}}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--danger-soft)",color:"var(--danger-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="trash" s={16}/>
          </div>
          <h3>Delete project</h3>
        </div>
        <div className="modal-body">
          <div className="banner danger" style={{marginBottom:14,background:"var(--danger-soft)",color:"var(--danger-ink)"}}>
            <div className="b-ico"><Icon n="alert-triangle" s={14}/></div>
            <div className="b-body">
              <span className="b-title">This cannot be undone</span>
              <b>{project.name}</b> and all its folders, WPIDs, versions, and sign-offs will be permanently removed.
            </div>
          </div>

          {/* Step 1 — backup */}
          <div className="field">
            <label>Step 1 · Download a backup</label>
            <div style={{display:"flex",alignItems:"center",gap:10}}>
              <button className="btn" onClick={()=>{ onZip(); setZipped(true); }}>
                <Icon n="download" s={13}/> Download project (.zip)
              </button>
              {zipped && (
                <span className="sbadge ok" style={{fontSize:11}}>
                  <Icon n="check-circle" s={10}/> Backup downloaded
                </span>
              )}
            </div>
            {!zipped && <div className="help">Download the full project archive before the delete step unlocks.</div>}
          </div>

          {/* Step 2 — type Tax ID, GitHub-style */}
          <div className="field" style={{marginBottom:0, opacity: zipped ? 1 : .45}}>
            <label>Step 2 · Type the client's Tax ID to confirm</label>
            <div className="help" style={{marginBottom:6}}>
              Type <span className="mono" style={{userSelect:"all"}}>{entity.taxId}</span> ({entity.name})
            </div>
            <input type="text" value={taxInput} disabled={!zipped}
              onChange={e=>setTaxInput(e.target.value)}
              placeholder={entity.taxId}
              style={{fontFamily:"var(--mono)", borderColor: !taxInput || match ? "var(--line)" : "var(--danger)"}}/>
            {taxInput && !match && <div className="err"><Icon n="alert" s={11}/> Tax ID does not match.</div>}
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn danger" disabled={!zipped || !match} onClick={onConfirm}
            style={(!zipped || !match) ? undefined : {background:"var(--danger)",borderColor:"var(--danger)",color:"#fff"}}>
            <Icon n="trash" s={13}/> Delete project permanently
          </button>
        </div>
      </div>
    </div>
  );
};

/* New folder — name + location, mirroring the SOP Template builder's modal */
const AddFolderModalP = ({ sections, defaultParentId, onClose, onSave }) => {
  const [name, setName] = React.useState("");
  const [parentId, setParentId] = React.useState(defaultParentId || sections[0]?.id || "");
  const valid = name.trim().length >= 2 && !!parentId;
  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--accent-soft)",color:"var(--accent-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="folder" s={16}/>
          </div>
          <h3>New folder</h3>
        </div>
        <div className="modal-body">
          <div className="field">
            <label>Location <span className="req">*</span></label>
            <select value={parentId} onChange={e=>setParentId(e.target.value)} autoFocus>
              {sections.map(s => (
                <option key={s.id} value={s.id}>
                  {" ".repeat(s.depth || 0)}{(s.depth || 0) > 0 ? "↳ " : ""}{s.name}
                </option>
              ))}
            </select>
            <div className="help">Pick any folder — sections or sub-folders — to nest the new folder under.</div>
          </div>
          <div className="field" style={{marginBottom:0}}>
            <label>Folder name <span className="req">*</span></label>
            <input type="text" value={name} onChange={e=>setName(e.target.value)} placeholder="e.g. 02.4 — Going Concern"/>
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={!valid} onClick={()=>onSave({ name: name.trim(), parentId })}>
            <Icon n="check" s={13}/> Create folder
          </button>
        </div>
      </div>
    </div>
  );
};

/* Move a WPID or folder — same location picker as the New-folder modal.
   `sections` arrives pre-filtered by the caller (no self/descendants). */
const MoveTargetModal = ({ title, itemName, sections, onClose, onMove }) => {
  const [target, setTarget] = React.useState(sections[0]?.id || "");
  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--accent-soft)",color:"var(--accent-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="folder-open" s={16}/>
          </div>
          <h3>{title}</h3>
        </div>
        <div className="modal-body">
          <div className="field">
            <label>Moving</label>
            <div className="dval"><b>{itemName}</b></div>
          </div>
          <div className="field" style={{marginBottom:0}}>
            <label>Destination <span className="req">*</span></label>
            <select value={target} onChange={e=>setTarget(e.target.value)} autoFocus>
              {sections.map(s => (
                <option key={s.id} value={s.id}>
                  {" ".repeat(s.depth || 0)}{(s.depth || 0) > 0 ? "↳ " : ""}{s.name}
                </option>
              ))}
            </select>
            <div className="help">Tip: you can also drag &amp; drop onto a folder in the tree.</div>
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={!target} onClick={()=>onMove(target)}>
            <Icon n="arrow-r" s={13}/> Move
          </button>
        </div>
      </div>
    </div>
  );
};

const AddWpidModalP = ({ folderName, onClose, onSave }) => {
  const [name, setName] = React.useState("");
  const [type, setType] = React.useState("docx");
  const valid = name.trim().length >= 2;
  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--accent-soft)",color:"var(--accent-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="plus" s={16}/>
          </div>
          <h3>Add WPID</h3>
        </div>
        <div className="modal-body">
          <div className="field">
            <label>WPID name <span className="req">*</span></label>
            <input type="text" value={name} onChange={e=>setName(e.target.value)} placeholder="e.g. Bank confirmations summary" autoFocus/>
            <div className="help">Will be added to folder <b>{folderName}</b>. Starts in <b>Pending</b> status.</div>
          </div>
          <div className="field" style={{marginBottom:0}}>
            <label>Expected file type <span className="req">*</span></label>
            <select value={type} onChange={e=>setType(e.target.value)}>
              <option value="docx">Word document (.docx)</option>
              <option value="xlsx">Excel spreadsheet (.xlsx)</option>
              <option value="pdf">PDF (.pdf)</option>
              <option value="txt">Plain text (.txt)</option>
            </select>
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={!valid} onClick={()=>onSave({name:name.trim(),type})}>
            <Icon n="plus" s={13}/> Add WPID
          </button>
        </div>
      </div>
    </div>
  );
};

const DiffModal = ({ wpid, versions, onClose }) => {
  const leftRef = React.useRef(null);
  const rightRef = React.useRef(null);

  /* default: left = most recent older version, right = current */
  const versionLabels = versions.map(h => h.v);
  const currentIdx = versions.findIndex(h => h.v.includes("(current)"));
  const defaultLeft = Math.min(versionLabels.length - 1, currentIdx + 1);
  const defaultRight = currentIdx >= 0 ? currentIdx : 0;

  const [leftV, setLeftV] = React.useState(versionLabels[defaultLeft] || versionLabels[0]);
  const [rightV, setRightV] = React.useState(versionLabels[defaultRight] || versionLabels[0]);

  /* source: current uses uploadedFile/previewUrl, older versions use sample (demo limit — no per-version file store) */
  const ext = "." + wpid.type;
  const sampleFallback = (typeof SAMPLE_BY_TYPE !== "undefined" ? SAMPLE_BY_TYPE[wpid.type] : null);
  const currentSource = wpid.uploadedFile || wpid.previewUrl || sampleFallback;

  const renderPanel = (target, vLabel) => {
    if (!target) return;
    if (!window.FilePreview) {
      target.innerHTML = `<div class="fp-error">FilePreview not loaded</div>`;
      return;
    }
    /* For current version use real source; for older versions use sample fallback (demo) */
    const isCurrent = vLabel === versionLabels[currentIdx];
    const src = isCurrent ? currentSource : sampleFallback;
    if (!src) {
      target.innerHTML = `<div class="fp-placeholder" style="min-height:160px"><div style="font-size:24px;opacity:.4">📄</div><div style="margin-top:6px;font-size:11.5px">No source for ${vLabel}</div></div>`;
      return;
    }
    window.FilePreview.render(target, { source: src, ext, name: `${wpid.name}_${vLabel}.${wpid.type}` });
  };

  React.useEffect(() => { renderPanel(leftRef.current, leftV); }, [leftV]);
  React.useEffect(() => { renderPanel(rightRef.current, rightV); }, [rightV]);

  const versionMetaOf = (label) => versions.find(h => h.v === label);

  return (
    <div className="modal-veil" onClick={onClose}>
      <div className="modal" onClick={e=>e.stopPropagation()} style={{maxWidth:"96vw",width:"96vw"}}>
        <div className="modal-head">
          <div style={{width:32,height:32,borderRadius:8,background:"var(--accent-soft)",color:"var(--accent-ink)",display:"grid",placeItems:"center"}}>
            <Icon n="history" s={16}/>
          </div>
          <div style={{flex:1,minWidth:0}}>
            <h3 style={{margin:0,fontSize:14}}>Compare versions — {wpid.name}</h3>
            <div className="mono muted" style={{fontSize:10.5,marginTop:2}}>Side-by-side</div>
          </div>
          <button className="icon-btn" onClick={onClose}><Icon n="x" s={16}/></button>
        </div>
        <div className="modal-body" style={{maxHeight:"78vh",overflowY:"auto",padding:14}}>
          <div className="banner muted" style={{marginBottom:12}}>
            <div className="b-ico"><Icon n="info" s={14}/></div>
            <div className="b-body">
              <b>Side-by-side compare</b> — Both versions render through the same engine ({window.FilePreview ? window.FilePreview.methodOf(ext) : "—"}). Only preview-renderable types support diff (no .doc / .xls).
            </div>
          </div>
          <div className="diff-grid">
            <div className="diff-panel">
              <div className="diff-panel-head">
                <span className="mono" style={{fontSize:10.5,color:"var(--muted)",textTransform:"uppercase"}}>Left · Version</span>
                <select className="btn sm" value={leftV} onChange={e=>setLeftV(e.target.value)} style={{paddingRight:24}}>
                  {versionLabels.map(v => <option key={v} value={v}>{v}</option>)}
                </select>
                {versionMetaOf(leftV) && (
                  <span className="mono" style={{fontSize:10.5,color:"var(--muted-2)"}}>
                    {versionMetaOf(leftV).who} · {versionMetaOf(leftV).t}
                  </span>
                )}
              </div>
              <div ref={leftRef} className="diff-panel-body"/>
            </div>
            <div className="diff-panel">
              <div className="diff-panel-head">
                <span className="mono" style={{fontSize:10.5,color:"var(--muted)",textTransform:"uppercase"}}>Right · Version</span>
                <select className="btn sm" value={rightV} onChange={e=>setRightV(e.target.value)} style={{paddingRight:24}}>
                  {versionLabels.map(v => <option key={v} value={v}>{v}</option>)}
                </select>
                {versionMetaOf(rightV) && (
                  <span className="mono" style={{fontSize:10.5,color:"var(--muted-2)"}}>
                    {versionMetaOf(rightV).who} · {versionMetaOf(rightV).t}
                  </span>
                )}
              </div>
              <div ref={rightRef} className="diff-panel-body"/>
            </div>
          </div>
        </div>
        <div className="modal-foot">
          <button className="btn" onClick={onClose}>Close</button>
        </div>
      </div>
    </div>
  );
};

const DeleteWpidModal = ({ wpid, onClose, onConfirm }) => (
  <div className="modal-veil" onClick={onClose}>
    <div className="modal" onClick={e=>e.stopPropagation()}>
      <div className="modal-head">
        <div style={{width:32,height:32,borderRadius:8,background:"var(--danger-soft)",color:"var(--danger-ink)",display:"grid",placeItems:"center"}}>
          <Icon n="trash" s={16}/>
        </div>
        <h3>Move WPID to Recycle Bin?</h3>
      </div>
      <div className="modal-body">
        <p style={{margin:0}}>
          <b>{wpid.name}</b> will be moved to Recycle Bin. You can restore it any time before an Admin permanently deletes it.
        </p>
      </div>
      <div className="modal-foot">
        <button className="btn" onClick={onClose}>Cancel</button>
        <button className="btn danger solid" onClick={onConfirm}>
          <Icon n="trash" s={13}/> Move to Recycle Bin
        </button>
      </div>
    </div>
  </div>
);

/* ---- Project Settings tab — edit project metadata on a live project ----
   Mirrors the project-creation form (admin_screens) so everything except the
   folder/template Structure can be re-configured after the project goes active.
   Reviewer-slot changes become the default sign-off chain for WPIDs that haven't
   started their own chain yet. Structure is intentionally omitted — it can't be
   swapped once WPIDs exist. */
const ProjectSettingsPanel = ({ project, entity, nav, onZip }) => {
  const [showDelete, setShowDelete] = React.useState(false);
  const [name, setName]               = React.useState(project.name || "");
  const [desc, setDesc]               = React.useState(project.description || "");
  const [serviceType, setServiceType] = React.useState(project.serviceType || "");
  const [periodStart, setPeriodStart] = React.useState(project.periodStart || "");
  const [periodEnd, setPeriodEnd]     = React.useState(project.periodEnd || "");
  const [cat, setCat]                 = React.useState(project.category || "");
  const [riskLevel, setRiskLevel]     = React.useState(project.riskLevel || "");
  const [members, setMembers]         = React.useState(project.members || []);
  const [slots, setSlots]             = React.useState(
    (project.reviewerSlots && project.reviewerSlots.length ? project.reviewerSlots : DEFAULT_REVIEWER_SLOTS).map(r => ({...r}))
  );
  const [pickerOpen, setPickerOpen]   = React.useState(false);
  const [dirty, setDirty]             = React.useState(false);
  const touch = () => setDirty(true);

  /* Reviewer picks restricted to project members (+ creator), non-Guest, active */
  const memberIdSet = new Set([project.createdBy, ...members]);
  const reviewerCandidates = USERS.filter(u => u.active && u.role !== "Guest" && memberIdSet.has(u.id));
  const serviceTypeObj = serviceTypeByName(serviceType);

  const updateSlot = (i, patch) => { setSlots(prev => prev.map((r, idx) => idx === i ? {...r, ...patch} : r)); touch(); };
  const addSlot    = () => { setSlots(prev => [...prev, { role:"Custom slot", required:false, userId:null }]); touch(); };
  const removeSlot = (i) => { setSlots(prev => prev.filter((_, idx) => idx !== i)); touch(); };
  const removeMember = (uid) => {
    setMembers(prev => prev.filter(m => m !== uid));
    setSlots(prev => prev.map(r => r.userId === uid ? {...r, userId: null} : r)); // drop from any slot
    touch();
  };

  /* Validation — same rules as project creation (MTG3 §2.2 / MTG4 §2.1) */
  const periodValid = !!periodStart && !!periodEnd && periodEnd >= periodStart;
  const periodDays  = periodValid ? Math.ceil((new Date(periodEnd) - new Date(periodStart)) / 86400000) + 1 : null;
  const valid = name.trim().length >= 2 && !!cat && !!riskLevel && !!serviceType && periodValid;

  const reset = () => {
    setName(project.name || ""); setDesc(project.description || "");
    setServiceType(project.serviceType || "");
    setPeriodStart(project.periodStart || ""); setPeriodEnd(project.periodEnd || "");
    setCat(project.category || ""); setRiskLevel(project.riskLevel || "");
    setMembers(project.members || []);
    setSlots((project.reviewerSlots || DEFAULT_REVIEWER_SLOTS).map(r => ({...r})));
    setDirty(false);
  };
  const save = () => {
    if (!valid) return;
    nav.editProjectFields(project.id, {
      name: name.trim(),
      description: desc.trim(),
      serviceType,
      periodStart, periodEnd,
      category: cat,
      riskLevel,
      members,
      reviewerSlots: slots.map(r => ({...r})),
    });
    nav.toast("Project settings updated", "ok");
    setDirty(false);
  };

  return (
    <div className="page-body">
      <div style={{maxWidth:780}}>

        {/* ── Project details ── */}
        <div className="sec">
          <div className="sec-head"><span className="sec-title"><Icon n="info" s={14}/> Project details</span></div>
          <div className="sec-body">
            <div className="field">
              <label>Project name <span className="req">*</span></label>
              <input type="text" value={name} onChange={e=>{setName(e.target.value); touch();}} placeholder="e.g. Annual Audit FY2026"/>
            </div>
            <div className="field">
              <label>Description</label>
              <textarea value={desc} onChange={e=>{setDesc(e.target.value); touch();}} placeholder="Scope, group consolidation notes, regulatory context…"/>
            </div>
            <div className="field">
              <label>
                <Tooltip text="Main engagement type (Audit / Limited Review / Agreed-Upon Procedures / Others).">
                  <span>Service Type <span className="req">*</span></span>
                </Tooltip>
              </label>
              <select value={serviceType} onChange={e=>{setServiceType(e.target.value); touch();}} title={serviceTypeObj?.desc}>
                <option value="">— Select service type —</option>
                {SERVICE_TYPE_CATALOG.map(st => <option key={st.code} value={st.name} title={st.desc}>{st.name}</option>)}
              </select>
              {serviceTypeObj && <div className="help" style={{marginTop:6}}>{serviceTypeObj.desc}</div>}
            </div>
            <div className="field">
              <label>
                <Tooltip text="Service Period is mandatory for every project. Pick the start and end dates the engagement covers.">
                  <span>Service Period <span className="req">*</span></span>
                </Tooltip>
              </label>
              <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:10}}>
                <div>
                  <div className="mono" style={{fontSize:10,color:"var(--muted)",marginBottom:4,textTransform:"uppercase",letterSpacing:".05em"}}>Start</div>
                  <input type="date" value={periodStart} onChange={e=>{setPeriodStart(e.target.value); touch();}} style={{fontFamily:"var(--mono)"}}/>
                </div>
                <div>
                  <div className="mono" style={{fontSize:10,color:"var(--muted)",marginBottom:4,textTransform:"uppercase",letterSpacing:".05em"}}>End</div>
                  <input type="date" value={periodEnd} onChange={e=>{setPeriodEnd(e.target.value); touch();}}
                    min={periodStart || undefined}
                    style={{fontFamily:"var(--mono)", borderColor: periodStart && periodEnd && periodEnd < periodStart ? "var(--danger)" : undefined}}/>
                </div>
              </div>
              {periodStart && periodEnd && periodEnd < periodStart && (
                <div className="err" style={{marginTop:6}}><Icon n="alert" s={11}/> End date must be on or after Start date.</div>
              )}
              {periodValid && (
                <div className="help" style={{marginTop:6}}><Icon n="check" s={11}/> Period covers <b>{periodDays}</b> day{periodDays !== 1 ? "s" : ""}.</div>
              )}
            </div>
            <div className="field">
              <label>
                <Tooltip text="Category drives sampling depth, EQR triggers, partner-review requirements, and regulatory reporting.">
                  <span>Category <span className="req">*</span></span>
                </Tooltip>
              </label>
              <select value={cat} onChange={e=>{setCat(e.target.value); touch();}} title={CATEGORY_INFO[cat]?.desc}>
                <option value="">— Select category —</option>
                {Object.entries(CATEGORY_INFO).map(([k,info]) => <option key={k} value={k} title={info.desc}>{info.label}</option>)}
              </select>
              {cat && CATEGORY_INFO[cat] && <div className="help" style={{marginTop:6,lineHeight:1.5}}><Icon n="info" s={11}/> {CATEGORY_INFO[cat].desc}</div>}
            </div>
            <div className="field" style={{marginBottom:0}}>
              <label>
                <Tooltip text="Drives sampling intensity, EQR triggers, and partner review depth.">
                  <span>Risk Level <span className="req">*</span></span>
                </Tooltip>
              </label>
              <select value={riskLevel} onChange={e=>{setRiskLevel(e.target.value); touch();}} title={RISK_INFO[riskLevel]?.desc}>
                <option value="">— Select risk level —</option>
                {RISK_LEVELS.map(r => <option key={r.v} value={r.v} title={r.desc}>{r.label}</option>)}
              </select>
              {riskLevel && <div className="help" style={{marginTop:6,lineHeight:1.5}}><Icon n="info" s={11}/> {RISK_INFO[riskLevel].desc}</div>}
            </div>
          </div>
        </div>

        {/* ── Members ── */}
        <div className="sec">
          <div className="sec-head"><span className="sec-title"><Icon n="users" s={14}/> Members</span><span className="badge">{members.length} selected</span></div>
          <div className="sec-body">
            <div className="usr-chips">
              {members.map(uid => {
                const u = userById(uid);
                return (
                  <span key={uid} className="usr-chip">
                    <Avatar id={uid} name={u.name} size="sm"/>
                    {u.name} <span className="muted" style={{fontSize:10}}>· {u.role}</span>
                    <span className="x" onClick={()=>removeMember(uid)}><Icon n="x" s={10}/></span>
                  </span>
                );
              })}
              <span className="usr-picker-add" onClick={()=>setPickerOpen(true)}>
                <Icon n="plus" s={11}/> Add member
              </span>
            </div>
            <div className="help" style={{marginTop:8}}>Pick from Supervisors and Staff. Removing a member also clears them from any reviewer role.</div>
          </div>
        </div>

        {/* ── Reviewer Slots ── */}
        <div className="sec">
          <div className="sec-head">
            <span className="sec-title">
              <Tooltip text="Define the reviewer roles that sign off on every WPID in this project. Each role = one seat held by one reviewer; if a position has two people (e.g. Manager #1 and Manager #2), add two roles. Required roles must sign before a WPID becomes Done; Optional roles can sign but don't block. Editing here updates the default chain for WPIDs that haven't started their own sign-off yet.">
                <span>Reviewer Roles <span className="badge">{slots.length}</span></span>
              </Tooltip>
            </span>
            <div className="sec-actions">
              <button className="btn sm" onClick={addSlot}><Icon n="plus" s={12}/> Add role</button>
            </div>
          </div>
          <div className="sec-body">
            <div style={{display:"flex",flexDirection:"column",gap:8}}>
              {slots.map((r, i) => (
                <div key={i} style={{
                  display:"grid",
                  gridTemplateColumns:"minmax(140px, 1fr) 180px minmax(180px, 1.2fr) 28px",
                  alignItems:"center",
                  gap:10,
                  padding:"8px 10px",
                  background:"var(--panel-3)",
                  border:"1px solid var(--line)",
                  borderRadius:"var(--radius)",
                }}>
                  <input
                    type="text"
                    value={r.role}
                    onChange={ev => updateSlot(i, { role: ev.target.value })}
                    placeholder="Role name (e.g. Manager #1)"
                    style={{fontWeight:500}}
                  />
                  <div className="seg" style={{display:"flex"}}>
                    <button onClick={() => updateSlot(i, { required: true })}
                      style={{padding:"6px 10px",fontSize:11.5,border:"1px solid var(--line)",borderRadius:"6px 0 0 6px",
                        background:r.required?"var(--ink)":"#fff",color:r.required?"#fff":"var(--ink-2)",cursor:"pointer"}}>
                      Required
                    </button>
                    <button onClick={() => updateSlot(i, { required: false })}
                      style={{padding:"6px 10px",fontSize:11.5,border:"1px solid var(--line)",borderLeft:"none",borderRadius:"0 6px 6px 0",
                        background:!r.required?"var(--ink)":"#fff",color:!r.required?"#fff":"var(--ink-2)",cursor:"pointer"}}>
                      Optional
                    </button>
                  </div>
                  <select value={r.userId || ""} onChange={ev => updateSlot(i, { userId: ev.target.value || null })}>
                    <option value="">— Unassigned —</option>
                    {reviewerCandidates.map(u => (
                      <option key={u.id} value={u.id}>{u.name} · {u.role}</option>
                    ))}
                  </select>
                  <button className="icon-btn" onClick={() => removeSlot(i)} title="Remove role" style={{color:"var(--danger-ink)"}}>
                    <Icon n="trash" s={12}/>
                  </button>
                </div>
              ))}
              {slots.length === 0 && (
                <div className="muted" style={{padding:"12px 4px",fontSize:12.5}}>
                  No reviewer roles defined. <button className="btn sm ghost" onClick={addSlot} style={{padding:"2px 6px"}}><Icon n="plus" s={11}/> Add one</button>
                </div>
              )}
            </div>
            <div className="help" style={{marginTop:10}}>
              Reviewer picks are restricted to <b>project members</b>. Assignments can stay unassigned and be filled in later. Existing WPIDs that already started their sign-off keep their own chain.
            </div>
          </div>
        </div>

        {/* ── Save bar ── */}
        <div style={{display:"flex",gap:8,alignItems:"center",marginTop:4}}>
          <button className="btn primary" disabled={!dirty || !valid} onClick={save}><Icon n="check" s={13}/> Save changes</button>
          <button className="btn ghost" disabled={!dirty} onClick={reset}>Reset</button>
          {dirty && !valid && <span className="err" style={{fontSize:12}}><Icon n="alert" s={12}/> Fill required fields (*) before saving.</span>}
          {dirty && valid && <span className="muted" style={{fontSize:12}}>Unsaved changes</span>}
        </div>

        {/* ── Danger zone — delete project (2-step confirm) ── */}
        <div className="sec" style={{marginTop:24,borderColor:"var(--danger-soft)"}}>
          <div className="sec-head">
            <span className="sec-title" style={{color:"var(--danger-ink)"}}><Icon n="alert-triangle" s={14}/> Danger zone</span>
          </div>
          <div className="sec-body" style={{display:"flex",alignItems:"center",gap:14,flexWrap:"wrap"}}>
            <div style={{flex:1,minWidth:240,fontSize:12.5,color:"var(--ink-2)"}}>
              Permanently delete this project with all folders, WPIDs, versions, and sign-offs.
              Requires a ZIP backup download and the client's Tax ID to confirm.
            </div>
            <button className="btn danger" onClick={()=>setShowDelete(true)}>
              <Icon n="trash" s={13}/> Delete project
            </button>
          </div>
        </div>
      </div>

      {showDelete && (
        <DeleteProjectModal project={project} entity={entity}
          onZip={onZip}
          onClose={()=>setShowDelete(false)}
          onConfirm={()=>{
            setShowDelete(false);
            nav.log && nav.log({ action:"delete", scope:project.id, obj:`Project ${project.name}`, detail:`Project deleted after ZIP backup + Tax ID confirmation`, severity:"critical" });
            nav.deleteProject(project.id);
          }}/>
      )}

      {pickerOpen && (
        <MemberPickerModal already={members} onClose={()=>setPickerOpen(false)} onSelect={(uid)=>{
          setMembers(prev => [...prev, uid]); setPickerOpen(false); touch();
        }}/>
      )}
    </div>
  );
};

/* ---- Structure Screen — 3-panel (collapsible) ---- */
const ProjectStructureScreen = ({ state, nav, projectId }) => {
  const [folderState, setFolderState] = React.useState(
    PROJECT_STRUCTURE.map(f => ({...f, open: f.open||false}))
  );
  const [activeFolder,  setActiveFolder]  = React.useState("F-022");
  /* Nothing is selected on entry — the folder list shows, but the WPID detail
     panel stays closed until the user actually picks a row (user decision 2026-06-12) */
  const [activeWpid,    setActiveWpid]    = React.useState(null);
  const [activeTab,     setActiveTab]     = React.useState("structure");
  const [detailTab,     setDetailTab]     = React.useState("overview"); // overview | signoff | task
  const [treeOpen,      setTreeOpen]      = React.useState(true);
  const [detailOpen,    setDetailOpen]    = React.useState(false);

  /* === live state for WPIDs / chains / notes / versions ===
     Seeds are per-project where defined (γ-flow coverage) and fall back to
     the default rich demo set. The screen is keyed by projectId in the
     router so state re-initialises when the project changes. */
  const [wpidsByFolder, setWpidsByFolder] = React.useState(() => WPID_SEEDS_BY_PROJECT[projectId] || DEFAULT_WPIDS_BY_FOLDER);
  const [signChainByWpid, setSignChainByWpid] = React.useState(() => CHAIN_SEEDS_BY_PROJECT[projectId] || SIGN_CHAINS_2200);
  const [notesByWpid, setNotesByWpid] = React.useState({ "2202": REVIEW_NOTES_2202 });
  /* Review Sheets keyed by WPID id; each value is a map slotIdx → rounds[] */
  const [sheetsByWpid, setSheetsByWpid] = React.useState({ "2202": REVIEW_SHEETS_2202 });
  const [expandedSheetSlot, setExpandedSheetSlot] = React.useState(null);
  const [versionsByWpid, setVersionsByWpid] = React.useState(() => ({ ...DEFAULT_VERSIONS_BY_WPID }));
  const [modal, setModal] = React.useState(null); // "preview" | "upload" | "addNote" | "addWpid" | "deleteWpid" | "webEdit" | "recycleBin"

  /* === Project-level search & status filter (MTG3 §2.4: moved to top-level) === */
  const [projectSearch, setProjectSearch] = React.useState("");
  const [statusFilter, setStatusFilter] = React.useState("all"); // all|pending|in-progress|in-review|done

  /* === Right-click context menu for folder/project ZIP download (MTG3 §2.4) === */
  const [ctxMenu, setCtxMenu] = React.useState(null); // {x,y,target:{type:'folder'|'project'|'wpid', id, name}}

  React.useEffect(() => {
    const close = () => setCtxMenu(null);
    window.addEventListener("click", close);
    window.addEventListener("scroll", close, true);
    return () => { window.removeEventListener("click", close); window.removeEventListener("scroll", close, true); };
  }, []);

  const openCtx = (ev, target) => {
    ev.preventDefault();
    ev.stopPropagation();
    setCtxMenu({ x: ev.clientX, y: ev.clientY, target });
  };

  const downloadZipMock = (target) => {
    const label = `${target.type === "project" ? "Project" : target.type === "folder" ? "Folder" : "WPID"} · ${target.name}`;
    nav.toast(`ZIP download started — ${label}`, "ok");
    nav.log && nav.log({ action:"download", scope:projectId, obj:label, detail:`Downloaded as ZIP archive` });
    setCtxMenu(null);
  };

  const project = state.projects.find(p => p.id === projectId);
  const entity  = project ? state.entities.find(e => e.id === project.entityId) : null;
  if (!project || !entity) return (
    <div className="page-body"><EmptyState icon="file" title="Project not found"/></div>
  );

  /* === γ Archive state derivation ===
     softLocked  → T0 (auditorReportDate) set and project still Active
     hardLocked  → project.status === "archived" (or archivedAt set)
     Both lock the workspace as read-only; permissions differ for the exit
     actions:
       Archive Now    → Supervisor (member of this project) or Admin
       Normal Unlock  → Admin only */
  const hardLocked = project.status === "archived" || !!project.archivedAt;
  const softLocked = !hardLocked && !!project.auditorReportDate;
  const softLockDaysLeft = (() => {
    if (!softLocked) return null;
    const t0 = new Date(project.auditorReportDate);
    const today = DEMO_TODAY || new Date();
    return Math.max(0, Math.ceil((t0.getTime() + 60*86400000 - today.getTime()) / 86400000));
  })();
  const isAdmin = CURRENT_ADMIN.role === "Admin";
  const isProjectSupervisor = CURRENT_ADMIN.role === "Supervisor" && project.members.includes(CURRENT_ADMIN.id);
  const canArchiveNow    = softLocked && (isAdmin || isProjectSupervisor);
  const canNormalUnlock  = softLocked && isAdmin;

  /* Required-sign-off readiness — walks every WPID in this project and
     checks all Required slots are signed. WPIDs with no explicit chain
     inherit project.reviewerSlots which begin "pending", so a fresh
     project (or fresh WPID) never reads as ready. Per req §4.12 the
     "Set Report Date" button is HARD-GATED on this (not just a warning). */
  const signReadiness = (() => {
    const wpidIds = Object.values(wpidsByFolder).flat().map(w => w.id);
    const readyIds = wpidIds.filter(id => {
      const c = signChainByWpid[id];
      return !!c && c.every(slot => !slot.required || slot.status === "signed");
    });
    return { total: wpidIds.length, done: readyIds.length, ready: wpidIds.length > 0 && readyIds.length === wpidIds.length };
  })();
  const requiredSignedReady = signReadiness.ready;

  const handleSetReportDate = (t0) => {
    nav.setReportDate(projectId, t0);
    setModal(null);
  };

  const handleArchiveNow = () => {
    if (!window.confirm("Archive this project now?\n\nThe project will Hard Lock immediately and all members except Admin will be removed.")) return;
    nav.archiveProjectNow(projectId);
  };

  const handleNormalUnlock = () => {
    if (!window.confirm("Normal Unlock?\n\nThe Soft Lock will be lifted and the project returns to Active — all edits become possible again. T0 will need to be re-set when work is finished.")) return;
    nav.normalUnlockProject(projectId);
  };

  const currentWpids = wpidsByFolder[activeFolder] || [];
  const wp = activeWpid ? (currentWpids.find(w => w.id === activeWpid) || null) : null;
  /* Chain falls back to project.reviewerSlots when no per-WPID seed exists,
     so every WPID inherits the project's slot structure on first render.
     Decorate each row with its Review-Sheet qaCount (rounds awaiting an
     answer) so the slot panel can flag "N Q&A" inline. */
  const baseChain = wp
    ? (signChainByWpid[wp.id] || (project.reviewerSlots && project.reviewerSlots.length ? project.reviewerSlots : DEFAULT_REVIEWER_SLOTS).map(s => ({
        role: s.role, required: s.required, userId: s.userId,
        name: s.userId ? (USERS.find(u => u.id === s.userId)?.name || null) : null,
        initials: s.userId ? (USERS.find(u => u.id === s.userId)?.name || "").split(" ").map(x => x[0]).join("").toUpperCase().slice(0,2) : null,
        status: s.required ? "pending" : "optional",
      })))
    : [];
  const wpSheets = wp ? (sheetsByWpid[wp.id] || {}) : {};
  const chain = baseChain.map((c, idx) => ({
    ...c,
    qaCount: (wpSheets[idx] || []).filter(r => !r.a).length,
  }));
  const notes    = wp ? (notesByWpid[wp.id] || []) : [];
  const versions = wp ? (versionsByWpid[wp.id] || []) : [];
  const openNotes = notes.filter(n => !n.resolved);
  const allRequiredDone = chain.length > 0 && chain.every(c => !c.required || c.status === "signed");

  /* Resolve "SM" → "Sarah Mitchell" for the Last-updated column */
  const nameByInitials = (ini) =>
    PROJECT_TEAM.find(m => m.initials === ini)?.name
    || USERS.find(u => u.name.split(" ").map(s => s[0]).join("").toUpperCase().slice(0, 2) === ini)?.name
    || ini;

  /* folder display name — recursive walk of the LIVE folderState so
     user-added folders (at any depth) resolve their name too */
  let folderLabel = activeFolder;
  let folderSub   = "";
  {
    const findIn = (nodes) => {
      for (const n of nodes) {
        if (n.id === activeFolder) return n;
        if (n.children) { const hit = findIn(n.children); if (hit) return hit; }
      }
      return null;
    };
    const node = findIn(folderState);
    if (node) {
      const isTop = folderState.some(f => f.id === node.id);
      folderLabel = node.name;
      folderSub   = `${currentWpids.length || node.items} WPIDs`
        + (isTop || String(node.id).startsWith("F-NEW-") ? "" : " · inherited from SOP template");
    }
  }

  const toggleFolder = (fid) => {
    setFolderState(prev => prev.map(f => f.id===fid ? {...f, open:!f.open} : f));
    setActiveFolder(fid);
    /* Switching folders clears the WPID selection — the detail panel only
       follows an explicit row click, never a folder change */
    setActiveWpid(null);
    setDetailOpen(false);
  };

  /* Folder-tree helpers — the tree nests to any depth (like the SOP Template
     builder), so inserts, id collection, and the modal's location list all
     walk it recursively. */
  const collectFolderIds = (n) => [n.id, ...(n.children || []).flatMap(collectFolderIds)];
  const flattenFolders = (nodes, depth = 0) =>
    nodes.flatMap(n => [{ id: n.id, name: n.name, depth }, ...flattenFolders(n.children || [], depth + 1)]);
  const insertChild = (nodes, parentId, child) =>
    nodes.map(n => n.id === parentId
      ? { ...n, open: true, children: [...(n.children || []), child] }
      : (n.children ? { ...n, children: insertChild(n.children, parentId, child) } : n));

  /* Create a folder under ANY folder the user picked in the New-folder modal
     (section or sub-folder). The new folder starts empty and becomes the
     selection so the Add-WPID empty state shows immediately. */
  const handleAddFolder = ({ name, parentId }) => {
    if (folderNameTaken(parentId, name)) {
      nav.toast(`A folder named "${name}" already exists there — pick another name`, "warn");
      return false;   /* keeps the modal open */
    }
    const newId = "F-NEW-" + Date.now().toString(36);
    setFolderState(prev => insertChild(prev, parentId, { id: newId, name, items: 0 }));
    setWpidsByFolder(prev => ({ ...prev, [newId]: [] }));
    setActiveFolder(newId);
    setActiveWpid(null);
    setDetailOpen(false);
    nav.toast(`Folder "${name}" added`, "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`Folder ${name}`, detail:`Folder added under ${parentId === "__root__" ? "top level" : folderPathById(parentId)}` });
    return true;
  };

  /* === Move WPID / folder — via context menu "Move to…" or drag & drop === */
  const [dragOverFolder, setDragOverFolder] = React.useState(null);
  const findFolderNode = (nodes, id) => {
    for (const n of nodes) {
      if (n.id === id) return n;
      if (n.children) { const hit = findFolderNode(n.children, id); if (hit) return hit; }
    }
    return null;
  };
  const removeFolderNode = (nodes, id) => {
    let removed = null;
    const walk = (list) => list
      .filter(n => { if (n.id === id) { removed = n; return false; } return true; })
      .map(n => n.children ? { ...n, children: walk(n.children) } : n);
    return [walk(nodes), () => removed];
  };
  /* Duplicate-name rule (user decision 2026-06-12, filesystem-style):
     names must be UNIQUE WITHIN a folder (files among siblings, folders
     among siblings) but MAY repeat across folders — audit sections
     legitimately reuse standard names like "Lead schedule".
     Enforced at create / move (modal + drag-drop). */
  const wpidNameTaken = (folderId, name, excludeId) =>
    (wpidsByFolder[folderId] || []).some(w => w.id !== excludeId && w.name.trim().toLowerCase() === name.trim().toLowerCase());
  const folderNameTaken = (parentId, name, excludeId) => {
    const siblings = (!parentId || parentId === "__root__")
      ? folderState
      : (findFolderNode(folderState, parentId)?.children || []);
    return siblings.some(n => n.id !== excludeId && n.name.trim().toLowerCase() === name.trim().toLowerCase());
  };
  /* Full folder path snapshot ("02 — RA › 02.2 — Materiality") — stamped
     into Activity Log events so log search survives duplicate names AND
     later renames (user decision 2026-06-12). */
  const folderPathById = (fid) => {
    const walk = (nodes, trail) => {
      for (const n of nodes) {
        if (n.id === fid) return [...trail, n.name];
        if (n.children) { const hit = walk(n.children, [...trail, n.name]); if (hit) return hit; }
      }
      return null;
    };
    return (walk(folderState, []) || [String(fid)]).join(" › ");
  };
  const moveWpidTo = (wpidId, fromId, toId) => {
    if (!toId || fromId === toId) return;
    const item = (wpidsByFolder[fromId] || []).find(w => w.id === wpidId);
    if (!item) return;
    if (wpidNameTaken(toId, item.name)) {
      nav.toast(`"${item.name}" already exists in ${folderNameById(toId)} — rename one first`, "warn");
      return;
    }
    setWpidsByFolder(prev => ({
      ...prev,
      [fromId]: (prev[fromId] || []).filter(w => w.id !== wpidId),
      [toId]: [...(prev[toId] || []), item],
    }));
    if (activeWpid === wpidId) setActiveFolder(toId);
    nav.toast(`"${item.name}" moved to ${folderNameById(toId)}`, "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${wpidId} ${item.name}`, detail:`Moved from ${folderPathById(fromId)} to ${folderPathById(toId)}` });
  };
  const moveFolderTo = (folderId, newParentId) => {
    if (!newParentId || folderId === newParentId) return;
    const node = findFolderNode(folderState, folderId);
    if (!node) return;
    if (newParentId !== "__root__" && collectFolderIds(node).includes(newParentId)) {
      nav.toast("Cannot move a folder into itself or its own sub-folder", "warn");
      return;
    }
    if (folderNameTaken(newParentId, node.name, folderId)) {
      nav.toast(`A folder named "${node.name}" already exists there — rename one first`, "warn");
      return;
    }
    setFolderState(prev => {
      const [without, getRemoved] = removeFolderNode(prev, folderId);
      const removed = getRemoved();
      if (!removed) return prev;
      return newParentId === "__root__"
        ? [...without, removed]
        : insertChild(without, newParentId, removed);
    });
    nav.toast(`Folder "${node.name}" moved`, "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`Folder ${node.name}`, detail:`Moved from ${folderPathById(folderId)} to ${newParentId === "__root__" ? "top level" : folderPathById(newParentId)}` });
  };
  /* shared drop-target props for tree nodes */
  const dropProps = (folderId) => ({
    onDragOver: (e) => { e.preventDefault(); setDragOverFolder(folderId); },
    onDragLeave: () => setDragOverFolder(cur => cur === folderId ? null : cur),
    onDrop: (e) => {
      e.preventDefault(); e.stopPropagation();
      setDragOverFolder(null);
      try {
        const d = JSON.parse(e.dataTransfer.getData("text/plain") || "{}");
        if (d.t === "wpid")   moveWpidTo(d.id, d.from, folderId);
        if (d.t === "folder") moveFolderTo(d.id, folderId);
      } catch { /* foreign drag — ignore */ }
    },
  });

  /* === Handlers === */
  const updateActiveWpid = (patch) => {
    setWpidsByFolder(prev => ({
      ...prev,
      [activeFolder]: prev[activeFolder].map(w => w.id === wp.id ? {...w, ...patch} : w),
    }));
  };

  /* Transitions follow Proud Workpapers PDF flow:
     In Progress → Waiting for Review → Assigned RN → Responded RN
       → (Rejected RN → loops back to In Progress) OR (Completed)              */
  const setWpidStatus = (newStatus, message) => {
    updateActiveWpid({ status: newStatus });
    nav.toast(message || `Status → ${WPID_STATUS_META[newStatus]?.label || newStatus}`, "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:`Status changed → ${WPID_STATUS_META[newStatus]?.label || newStatus}` });
  };

  const handleSubmitForReview     = () => setWpidStatus("in-review",   "Submitted for review — reviewers have been notified");
  const handleAssignReviewNotes   = () => setWpidStatus("in-review",   "Review notes assigned to preparer");
  const handleRespondReviewNotes  = () => setWpidStatus("in-review",   "Response sent");
  const handleAcceptResponse      = () => {
    if (openNotes.length > 0) { nav.toast("Resolve all open Review Notes first", "warn"); return; }
    if (!allRequiredDone)     { nav.toast("All required signers must sign first", "warn"); return; }
    updateActiveWpid({ status: "done" });
    nav.toast("WPID marked as Done", "ok");
    nav.log && nav.log({ action:"sign", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:"WPID Done — all required signers complete · review notes resolved", severity:"critical" });
  };
  const handleRejectResponse      = () => setWpidStatus("in-progress", "Response rejected — sent back to preparer");
  const handleBackToInProgress    = () => setWpidStatus("in-progress", "Status → In Progress");

  /* Legacy compat for existing callers (renamed buttons below use specific handlers) */
  const handleMarkDone = handleAcceptResponse;

  const handleSign = (idx) => {
    const adminInitials = CURRENT_ADMIN.name.split(" ").map(s=>s[0]).join("").toUpperCase().slice(0,2);
    setSignChainByWpid(prev => ({
      ...prev,
      [wp.id]: (prev[wp.id] || chain).map((c, i) => i === idx
        ? { ...c, status: "signed", version: wp.v, signedAt: "just now", stale: false,
            name: c.name || CURRENT_ADMIN.name,
            initials: c.initials || adminInitials,
            userId: c.userId || CURRENT_ADMIN.id }
        : c),
    }));
    nav.toast(`Signed as ${chain[idx].role}`, "ok");
    nav.log && nav.log({ action:"sign", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:`Signed-off as ${chain[idx].role} on ${wp.v}`, severity:"critical" });
  };

  /* Submit a new review on a Review Sheet (Slot × WPID). One open review at a time. */
  const handleAskQuestion = (slotIdx) => {
    const q = window.prompt("New review for this reviewer sheet:");
    if (!q || !q.trim()) return;
    const newRound = {
      id: "QA-" + Date.now(),
      qBy: CURRENT_ADMIN.id, qByName: CURRENT_ADMIN.name, qAt: "just now",
      q: q.trim(),
    };
    setSheetsByWpid(prev => {
      const wpSheets = prev[wp.id] || {};
      const slot = wpSheets[slotIdx] || [];
      return { ...prev, [wp.id]: { ...wpSheets, [slotIdx]: [...slot, newRound] } };
    });
    nav.toast("Review submitted — waiting for a response", "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:`Asked Q on slot ${chain[slotIdx]?.role}`, severity:"normal" });
  };

  const handleAnswerQuestion = (slotIdx, roundIdx) => {
    const a = window.prompt("Your response to this review:");
    if (!a || !a.trim()) return;
    setSheetsByWpid(prev => {
      const wpSheets = prev[wp.id] || {};
      const slot = wpSheets[slotIdx] || [];
      const updated = slot.map((r, i) => i === roundIdx
        ? { ...r, a: a.trim(), aBy: CURRENT_ADMIN.id, aByName: CURRENT_ADMIN.name, aAt: "just now" }
        : r);
      return { ...prev, [wp.id]: { ...wpSheets, [slotIdx]: updated } };
    });
    nav.toast("Response recorded", "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:`Responded to review on slot ${chain[slotIdx]?.role}`, severity:"normal" });
  };

  /* Cancel a sign-off — only the original signer can cancel, only before T0
     (project.auditorReportDate is the proxy for T0 in this POC). */
  const handleCancelSign = (idx) => {
    const slot = chain[idx];
    if (!slot || slot.status !== "signed") return;
    if (slot.userId !== CURRENT_ADMIN.id) {
      nav.toast("Only the original signer can cancel this sign-off", "warn");
      return;
    }
    if (project.auditorReportDate) {
      nav.toast("Cancel disabled — countdown to Archive has started", "warn");
      return;
    }
    setSignChainByWpid(prev => ({
      ...prev,
      [wp.id]: (prev[wp.id] || chain).map((c, i) => i === idx
        ? { ...c, status: "pending", version: null, signedAt: null, stale: false }
        : c),
    }));
    nav.toast(`Sign-off cancelled — ${slot.role} returns to pending`, "warn");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:`Cancelled own sign-off (${slot.role})`, severity:"critical" });
  };

  /* Add an extra Reviewer Slot to THIS WPID only. Required reopens a Done
     WPID; Optional doesn't affect WPID state. */
  const handleAddSlot = () => {
    const role = window.prompt("Role name (e.g. Reviewer #2):", "Reviewer #2");
    if (!role || !role.trim()) return;
    const required = window.confirm("Make this slot Required?\n\nRequired → if the WPID is already Done, it will reopen to In Review.\nOptional → won't affect WPID state.");
    const newSlot = { role: role.trim(), required, userId: null, name: null, initials: null, status: required ? "pending" : "optional" };
    setSignChainByWpid(prev => ({
      ...prev,
      [wp.id]: [...(prev[wp.id] || chain), newSlot],
    }));
    if (required && wp.status === "done") {
      updateActiveWpid({ status: "in-review" });
      nav.toast(`Added Required slot — WPID reopened to In Review`, "warn");
    } else {
      nav.toast(`Added ${required ? "Required" : "Optional"} slot to this WPID`, "ok");
    }
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:`Added ${required ? "Required" : "Optional"} slot "${role.trim()}"`, severity:"critical" });
  };

  /* Restore an old version — append-only: creates a NEW version that is a
     copy of the old one ("Restored from vX"); history is never rewritten.
     Signed slots go stale exactly like a fresh upload. */
  /* Confirmation happens in RestoreVersionModal (system modal, not
     window.confirm) — this runs after the user confirms there. */
  const handleRestoreVersion = (h) => {
    const fromV = h.v.replace(" (current)", "");
    const currentV = wp.v === "—" ? "v0" : wp.v;
    const num = parseInt(currentV.replace(/\D/g, ""), 10) || 0;
    const newV = "v" + (num + 1);
    const adminInitials = CURRENT_ADMIN.name.split(" ").map(s=>s[0]).join("").toUpperCase().slice(0,2);
    setVersionsByWpid(prev => {
      const old = (prev[wp.id] || []).map(x => x.v.includes("(current)") ? {...x, v: x.v.replace(" (current)","")} : x);
      return {
        ...prev,
        [wp.id]: [
          { v: newV + " (current)", who: adminInitials, t: "just now", note: `Restored from ${fromV}` },
          ...old.slice(0, 2),
        ],
      };
    });
    setSignChainByWpid(prev => ({
      ...prev,
      [wp.id]: (prev[wp.id] || []).map(c => c.status === "signed" ? {...c, stale: true} : c),
    }));
    updateActiveWpid({ v: newV, updated: "just now" });
    nav.toast(`Restored ${fromV} as ${newV}`, "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:`Restored version ${fromV} as ${newV}`, severity:"critical" });
  };

  const handleUpload = ({ note, file }) => {
    const currentV = wp.v === "—" ? "v0" : wp.v;
    const num = parseInt(currentV.replace(/\D/g,""), 10) || 0;
    const newV = "v" + (num + 1);
    const adminInitials = CURRENT_ADMIN.name.split(" ").map(s=>s[0]).join("").toUpperCase().slice(0,2);
    const fmtSize = (b) => b < 1024 ? b + " B" : b < 1048576 ? (b/1024).toFixed(1) + " KB" : (b/1048576).toFixed(1) + " MB";

    setVersionsByWpid(prev => {
      const old = (prev[wp.id] || []).map(h => h.v.includes("(current)") ? {...h, v: h.v.replace(" (current)","")} : h);
      return {
        ...prev,
        [wp.id]: [
          { v: newV + " (current)", who: adminInitials, t: "just now", note: note || (file ? `Uploaded ${file.name}` : "New upload") },
          ...old.slice(0, 2),
        ],
      };
    });
    setSignChainByWpid(prev => ({
      ...prev,
      [wp.id]: (prev[wp.id] || []).map(c => c.status === "signed" ? {...c, stale: true} : c),
    }));

    const patch = {
      v: newV,
      updated: "just now",
      size: file ? fmtSize(file.size) : (wp.size === "—" ? "86 KB" : wp.size),
      /* New uploads always reset to "In Progress" per Proud Workpapers flow */
      status: normalizeWpidStatus(wp.status) === "completed" ? "in-progress" : normalizeWpidStatus(wp.status),
    };
    /* MTG4 §2.3 — auto-stamp Prepared by on FIRST upload only */
    if (!wp.preparedBy) {
      patch.preparedBy = CURRENT_ADMIN.id;
      patch.preparedByName = CURRENT_ADMIN.name;
      patch.preparedAt = new Date().toISOString();
    }
    /* store the real file for live preview */
    if (file) {
      patch.uploadedFile = file;
      /* if the file extension differs, update type so renderer picks the right engine */
      const ext = (file.name.split(".").pop() || "").toLowerCase();
      if (ext) patch.type = ext;
    }
    updateActiveWpid(patch);

    setModal(null);
    nav.toast(`Uploaded ${newV}${file ? " · " + file.name : ""}`, "ok");
    nav.log && nav.log({
      action:"upload",
      scope:projectId,
      obj:`WP-${wp.id} ${wp.name}`,
      detail:`Uploaded ${newV}${file ? " · " + file.name + " · " + patch.size : ""}${note ? " · note: " + note : ""}`,
    });
  };

  const handleAddNote = (payload) => {
    /* payload: { subject, text, assigneeId, targetWpid, screenshots } */
    const adminInitials = CURRENT_ADMIN.name.split(" ").map(s=>s[0]).join("").toUpperCase().slice(0,2);
    const assignee = PROJECT_TEAM.find(m => m.id === payload.assigneeId) || userById(payload.assigneeId);
    const newNote = {
      id: "RN-" + Math.random().toString(36).slice(2,7),
      type: "note",
      userId: CURRENT_ADMIN.id,
      name: CURRENT_ADMIN.name,
      initials: adminInitials,
      subject: payload.subject,
      text: payload.text,
      assigneeId: payload.assigneeId,
      assigneeName: assignee?.name,
      screenshots: payload.screenshots || [],
      wpidHistory: [{wpid: payload.targetWpid, at: "just now"}],
      ts: "just now",
      resolved: false,
    };
    setNotesByWpid(prev => ({
      ...prev,
      [payload.targetWpid]: [...(prev[payload.targetWpid] || []), newNote],
    }));
    setModal(null);
    nav.toast("Review note posted", "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${payload.targetWpid} ${payload.subject}`,
      detail:`Review note: "${payload.subject}" → ${assignee?.name || "?"}${payload.screenshots?.length ? ` · ${payload.screenshots.length} screenshot(s)` : ""}` });
  };

  /* MTG4 §2.4 — reassign a Review Note to a different WPID, log to history */
  const handleReassignNote = (noteId, fromWpid, toWpid) => {
    const note = (notesByWpid[fromWpid] || []).find(n => n.id === noteId);
    if (!note) return;
    const moved = { ...note, wpidHistory: [...(note.wpidHistory||[]), {wpid: toWpid, at: "just now", note: `reassigned by ${CURRENT_ADMIN.name}`}] };
    setNotesByWpid(prev => ({
      ...prev,
      [fromWpid]: (prev[fromWpid] || []).filter(n => n.id !== noteId),
      [toWpid]:   [...(prev[toWpid] || []), moved],
    }));
    nav.toast(`Review note reassigned to WP-${toWpid}`, "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`Review Note ${noteId}`,
      detail:`Reassigned WP-${fromWpid} → WP-${toWpid}`, severity:"critical" });
  };

  const handleResolveNote = (noteId) => {
    setNotesByWpid(prev => ({
      ...prev,
      [wp.id]: (prev[wp.id] || []).map(n => n.id === noteId ? {...n, resolved: true} : n),
    }));
    nav.toast("Note marked resolved", "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:"Review note resolved" });
  };

  const handleAddWpid = ({ name, type }) => {
    if (wpidNameTaken(activeFolder, name)) {
      nav.toast(`"${name}" already exists in this folder — pick another name`, "warn");
      return;   /* modal stays open */
    }
    const newId = Math.floor(2200 + currentWpids.length + Math.random()*99).toString();
    const adminInitials = CURRENT_ADMIN.name.split(" ").map(s=>s[0]).join("").toUpperCase().slice(0,2);
    const newWpid = {
      id: newId,
      name, type,
      status: "in-progress",   /* PDF: WPIDs start as "In progress" (เริ่มจัดทำ) */
      owner: adminInitials,
      v: "—",
      updated: "—",
      size: "—",
    };
    setWpidsByFolder(prev => ({
      ...prev,
      [activeFolder]: [...(prev[activeFolder] || []), newWpid],
    }));
    setActiveWpid(newId);
    setDetailOpen(true);
    setDetailTab("overview");
    setModal(null);
    nav.toast(`WPID "${name}" added`, "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${newId} ${name}`, detail:`WPID added in ${folderPathById(activeFolder)} · expects .${type}` });
  };

  /* Web-based edit handlers per MTG3 §2.1 + Draft model (DESIGN Web Editor
     Architecture §2.5): lock on open; CLOSE keeps a dangling draft (not a
     version); only "save version" or discard clears it. */
  const openEditorNow = (target) => {
    setWpidsByFolder(prev => ({
      ...prev,
      [activeFolder]: prev[activeFolder].map(w => w.id === target.id
        ? { ...w, lockedBy: CURRENT_ADMIN.id, lockedByName: CURRENT_ADMIN.name, lockedAt: new Date().toISOString() }
        : w),
    }));
    setModal("webEdit");
    nav.toast(`Opened Web Editor — ${target.name} now locked to you`, "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${target.id} ${target.name}`, detail:"Opened in Web Editor — file locked" });
  };
  const handleOpenWebEditor = (target) => {
    /* someone else's dangling draft holds the file's web-edit slot */
    if (target.draftBy && target.draftBy !== CURRENT_ADMIN.id) return;
    /* my draft is stale — current moved on (e.g. someone uploaded) → choose */
    if (target.draftBy === CURRENT_ADMIN.id && target.draftBaseV && target.draftBaseV !== target.v) {
      setModal({ kind: "staleDraft" });
      return;
    }
    openEditorNow(target);
  };
  /* Owner or Admin clears a dangling draft — Admin discarding someone
     else's work is a critical event (DESIGN §2.5) */
  const handleDiscardDraft = () => {
    const mine = wp.draftBy === CURRENT_ADMIN.id;
    const byName = wp.draftByName;
    updateActiveWpid({ draftBy: null, draftByName: null, draftAt: null, draftBaseV: null });
    setModal(null);
    nav.toast(mine ? "Draft discarded" : `Draft by ${byName} discarded`, "warn");
    nav.log && nav.log({ action:"delete", scope:projectId, obj:`WP-${wp.id} ${wp.name}`,
      detail: mine ? "Discarded own web-edit draft" : `Admin discarded web-edit draft by ${byName}`,
      severity: mine ? "normal" : "critical" });
  };

  const handleWebEditSave = (newContent) => {
    const num = parseInt((wp.v||"v0").replace(/\D/g,""), 10) || 0;
    const newV = "v" + (num + 1);
    setVersionsByWpid(prev => {
      const old = (prev[wp.id] || []).map(h => h.v.includes("(current)") ? {...h, v: h.v.replace(" (current)","")} : h);
      const adminInitials = CURRENT_ADMIN.name.split(" ").map(s=>s[0]).join("").toUpperCase().slice(0,2);
      return {
        ...prev,
        [wp.id]: [
          { v: newV + " (current)", who: adminInitials, t: "just now", note: "Edited on web (SharePoint-style)" },
          ...old.slice(0, 2),
        ],
      };
    });
    /* release lock + clear draft + bump version + persist web edit content */
    updateActiveWpid({
      v: newV,
      updated: "just now",
      webEditContent: newContent,
      lockedBy: null, lockedByName: null, lockedAt: null,
      draftBy: null, draftByName: null, draftAt: null, draftBaseV: null,
      status: wp.status === "pending" ? "in-progress" : wp.status,
    });
    setModal(null);
    nav.toast(`Saved ${newV} — lock released`, "ok");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:`Web-edit saved as ${newV} · lock released` });
  };

  const handleWebEditClose = () => {
    /* close ≠ save: the session lock is released but the work survives as a
       DANGLING DRAFT bound to me — others still can't web-edit (1 file =
       1 person) though upload stays open (user decision 2026-06-13) */
    updateActiveWpid({
      lockedBy: null, lockedByName: null, lockedAt: null,
      draftBy: CURRENT_ADMIN.id, draftByName: CURRENT_ADMIN.name, draftAt: "just now", draftBaseV: wp.v,
    });
    setModal(null);
    nav.toast("Closed — kept as your draft (not a version yet)", "warn");
    nav.log && nav.log({ action:"edit", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:"Web Editor closed — work kept as dangling draft (no version)" });
  };

  const handleDeleteWpid = () => {
    const remaining = (wpidsByFolder[activeFolder] || []).filter(w => w.id !== wp.id);
    const deleted = wp;
    setWpidsByFolder(prev => ({ ...prev, [activeFolder]: remaining }));
    setModal(null);
    if (remaining.length > 0) setActiveWpid(remaining[0].id);
    else setDetailOpen(false);
    nav.toast(`"${deleted.name}" moved to Recycle Bin`, "warn");
    nav.log && nav.log({ action:"delete", scope:projectId, obj:`WP-${deleted.id} ${deleted.name}`, detail:`Soft-deleted from ${folderPathById(activeFolder)} · moved to Recycle Bin · recoverable`, severity:"critical" });
  };

  /* === Project-level filter applied to currentWpids (per MTG3 §2.4 — top-level search) === */
  const [wpSort, setWpSort] = React.useState({ k: null, dir: "asc" });
  const WP_STATUS_ORDER = { "pending": 0, "in-progress": 1, "in-review": 2, "done": 3 };
  const WP_SORT_GETTERS = {
    name:    w => w.name,
    status:  w => WP_STATUS_ORDER[normalizeWpidStatus(w.status)] ?? -1,
    updated: w => {
      if (!w.updated || w.updated === "—") return "";
      const [d, m, rest] = w.updated.split("/");           // dd/mm/yyyy hh:mm:ss
      const [y, time] = (rest || "").split(" ");
      return `${y}${m}${d}${time || ""}`;                  // yyyymmddhh:mm:ss — sortable
    },
  };
  /* Search = typeahead picker (3+ chars → dropdown of matches from EVERY
     folder; picking one jumps straight to it). It never filters the table —
     the table is folder-scoped, and the Status chips filter within the
     active folder only. (user decision 2026-06-12: cross-folder table
     filtering was confusing) */
  const folderNameById = (fid) => {
    const findIn = (nodes) => {
      for (const n of nodes) {
        if (n.id === fid) return n;
        if (n.children) { const hit = findIn(n.children); if (hit) return hit; }
      }
      return null;
    };
    return findIn(folderState)?.name || fid;
  };
  /* Smart query — GitHub/Gmail-style qualifiers: `status:done`,
     `status:"in progress"`, mixable with free text ("status:done risk").
     The qualifier matches loosely (status:prog → In Progress). */
  const parsedQuery = (() => {
    let statusRaw = null;
    const text = projectSearch.replace(/status:\s*("[^"]*"|\S+)/gi, (_, v) => {
      statusRaw = v.replace(/"/g, "");
      return "";
    }).trim();
    const norm = (s) => (s || "").toLowerCase().replace(/[^a-z]/g, "");
    const statusKey = statusRaw
      ? WPID_STATUS_KEYS.find(k => norm(k).includes(norm(statusRaw)) || norm(WPID_STATUS_META[k].label).includes(norm(statusRaw)))
      : null;
    return { text, statusRaw, statusKey };
  })();
  const searchActive = !!parsedQuery.statusKey || parsedQuery.text.length >= 3;
  /* user typed "status:" (or an unrecognized value) → offer the 4 statuses */
  const statusSuggest = /status:?\S*\s*$/i.test(projectSearch) && !parsedQuery.statusKey;
  const searchHits = searchActive
    ? Object.entries(wpidsByFolder)
        .flatMap(([fid, list]) => list.map(w => ({ ...w, _folderId: fid })))
        .filter(w =>
          (!parsedQuery.statusKey || normalizeWpidStatus(w.status) === parsedQuery.statusKey) &&
          (!parsedQuery.text || w.name.toLowerCase().includes(parsedQuery.text.toLowerCase())))
        .slice(0, 8)
    : [];
  const jumpToWpid = (w) => {
    if (w._folderId !== activeFolder) {
      setActiveFolder(w._folderId);
      /* expand the top-level branch so the tree shows where we jumped */
      setFolderState(prev => prev.map(f =>
        collectFolderIds(f).includes(w._folderId) ? { ...f, open: true } : f));
    }
    /* clear any lingering status filter — otherwise the jumped-to WPID can
       land in a folder where the filter hides every row ("nothing changed") */
    setStatusFilter("all");
    setActiveWpid(w.id); setDetailOpen(true); setDetailTab("overview");
    setProjectSearch("");
  };
  const filteredWpids = sortRows(currentWpids.filter(w =>
    statusFilter === "all" || normalizeWpidStatus(w.status) === statusFilter
  ), wpSort, WP_SORT_GETTERS);

  return (
    <>
      <ProjectHead project={project} entity={entity}
        softLocked={softLocked}
        hardLocked={hardLocked}
        signReadiness={signReadiness}
        onSetReportDate={() => setModal("setReportDate")}
        onZipProject={() => downloadZipMock({ type:"project", id: projectId, name: project.name })}
        onOpenRecycleBin={() => nav.go({ view: "recycle-bin", projectId })}
        pinned={(state.pinnedIds || []).includes(projectId)}
        onTogglePin={() => nav.togglePin(projectId)}
        wpStats={(() => {
          const all = Object.values(wpidsByFolder).flat();
          const counts = all.reduce((acc, w) => {
            const k = normalizeWpidStatus(w.status);
            acc[k] = (acc[k] || 0) + 1;
            return acc;
          }, {});
          const done = counts.done || 0;
          return {
            total: all.length, done,
            pct: all.length ? Math.round(done / all.length * 100) : 0,
            inProgress: counts["in-progress"] || 0,
            inReview: counts["in-review"] || 0,
            pending: counts.pending || 0,
          };
        })()}
        onContextMenu={(ev) => openCtx(ev, { type:"project", id: projectId, name: project.name })}/>

      {softLocked && !hardLocked && (
        <SoftLockBanner project={project} daysToLock={softLockDaysLeft}
          canArchiveNow={canArchiveNow}
          canNormalUnlock={canNormalUnlock}
          onArchiveNow={handleArchiveNow}
          onNormalUnlock={handleNormalUnlock}/>
      )}
      {hardLocked && (
        <HardLockBanner project={project} isAdmin={isAdmin}
          onUnlock={() => {
            if (!window.confirm("Revert Hard Lock?\n\nThe project will return to Active state. Removed members will need Request Access to rejoin.")) return;
            nav.editProjectFields(projectId, { status:"active", archivedAt:null, archivedBy:null, auditorReportDate:null });
            nav.toast("Project unlocked — back to Active","ok");
          }}/>
      )}

      <ProjectTabs activeTab={activeTab} onTab={setActiveTab}/>

      {/* Project-level Search + Status filter — moved from WPID list per MTG3 §2.4 */}
      {activeTab === "structure" && (
        <div style={{padding:"10px 24px",borderBottom:"1px solid var(--line)",background:"var(--panel-3)",display:"flex",gap:10,alignItems:"center",flexWrap:"wrap"}}>
          <div style={{position:"relative", maxWidth:340, flex:1}}>
            <div className="input">
              <Icon n="search" s={13} c="muted"/>
              <input
                placeholder="Jump to a WPID…"
                value={projectSearch}
                onChange={e=>setProjectSearch(e.target.value)}
                onKeyDown={e=>{ if (e.key === "Escape") setProjectSearch(""); }}
              />
            </div>
            {/* "status:" typed but no value recognized yet → suggest the 4 statuses */}
            {statusSuggest && (
              <div style={{position:"absolute",top:"calc(100% + 4px)",left:0,right:0,zIndex:31,
                background:"#fff",border:"1px solid var(--line)",borderRadius:"var(--radius)",overflow:"hidden"}}>
                {WPID_STATUS_KEYS.map(k => {
                  const m = WPID_STATUS_META[k];
                  return (
                    <div key={k}
                      onMouseDown={()=>setProjectSearch(projectSearch.replace(/status:?\S*\s*$/i, `status:${k} `))}
                      style={{padding:"7px 11px",cursor:"pointer",display:"flex",alignItems:"center",gap:8,borderBottom:"1px solid var(--line)"}}
                      onMouseEnter={e=>e.currentTarget.style.background="var(--panel-2)"}
                      onMouseLeave={e=>e.currentTarget.style.background=""}>
                      <span className="mono" style={{fontSize:11.5,color:"var(--ink-2)"}}>status:{k}</span>
                      <span style={{marginLeft:"auto"}}>{wpStatus(k)}</span>
                    </div>
                  );
                })}
              </div>
            )}
            {/* Typeahead dropdown — matches from every folder; pick to jump */}
            {!statusSuggest && searchHits.length > 0 && (
              <div style={{position:"absolute",top:"calc(100% + 4px)",left:0,right:0,zIndex:30,
                background:"#fff",border:"1px solid var(--line)",borderRadius:"var(--radius)",
                overflow:"hidden",maxHeight:300,overflowY:"auto"}}>
                {searchHits.map(w => (
                  <div key={w._folderId + "-" + w.id}
                    onMouseDown={()=>jumpToWpid(w)}
                    style={{padding:"7px 11px",cursor:"pointer",display:"flex",alignItems:"center",gap:8,borderBottom:"1px solid var(--line)"}}
                    onMouseEnter={e=>e.currentTarget.style.background="var(--panel-2)"}
                    onMouseLeave={e=>e.currentTarget.style.background=""}>
                    <Icon n={fileIc(w.type)} s={13} c="muted"/>
                    <div style={{flex:1,minWidth:0}}>
                      <div className="truncate" style={{fontSize:12.5,fontWeight:500}}>{w.name}</div>
                      <div className="mono truncate" style={{fontSize:10,color:"var(--muted)"}}>{folderNameById(w._folderId)}</div>
                    </div>
                    {wpStatus(w.status)}
                  </div>
                ))}
              </div>
            )}
            {!statusSuggest && searchActive && searchHits.length === 0 && (
              <div style={{position:"absolute",top:"calc(100% + 4px)",left:0,right:0,zIndex:30,
                background:"#fff",border:"1px solid var(--line)",borderRadius:"var(--radius)",
                padding:"9px 11px",fontSize:12,color:"var(--muted)"}}>
                No WPIDs match "{projectSearch.trim()}" in any folder.
              </div>
            )}
          </div>
          <div style={{display:"flex",alignItems:"center",gap:4,flexWrap:"wrap"}}>
            <span className="mono" style={{fontSize:10.5,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".05em",marginRight:4}}>Status</span>
            <button className={`btn sm ${statusFilter==="all"?"primary":""}`} onClick={()=>setStatusFilter("all")}>All</button>
            {WPID_STATUS_KEYS.map(k => {
              const m = WPID_STATUS_META[k];
              /* shorten label for chip: "Assigned Review Notes" → "Assigned RN" */
              const shortLabel = m.label
                .replace("Review Notes", "RN")
                .replace("Waiting for", "Waiting");
              return (
                <button key={k} className={`btn sm ${statusFilter===k?"primary":""}`} onClick={()=>setStatusFilter(k)} title={m.detail}>
                  <Icon n={m.icon} s={11}/> {shortLabel}
                </button>
              );
            })}
          </div>
          {/* (no trailing counter — the folder header already shows the WPID count) */}
        </div>
      )}

      {activeTab === "structure" && (
        <div style={{
          display:"grid",
          gridTemplateColumns: [
            treeOpen   ? "260px" : "36px",
            "1fr",
            detailOpen ? "340px" : "0px",
          ].join(" "),
          minHeight:"calc(100vh - 200px)",
          transition:"grid-template-columns .2s ease",
          overflow:"hidden",
        }}>

          {/* ── Left: folder tree ─────────────────────────────── */}
          <div style={{borderRight:"1px solid var(--line)",background:"var(--panel-3)",display:"flex",flexDirection:"column",overflow:"hidden",minWidth:0}}>
            <div className="tree-head" style={{flexShrink:0,gap:6}}>
              {/* Add folder lives up here so it's visible without scrolling the tree */}
              {treeOpen && (
                <button className="btn sm primary" style={{flex:1,justifyContent:"center"}}
                  title="Add a folder — all roles can add folders (MTG3 §2.3)"
                  onClick={()=>setModal("addFolder")}>
                  <Icon n="plus" s={11}/> Add folder
                </button>
              )}
              <button
                className="btn ghost sm"
                style={{padding:"3px 5px",marginLeft:"auto",flexShrink:0}}
                title={treeOpen?"Collapse tree":"Expand tree"}
                onClick={()=>setTreeOpen(v=>!v)}>
                <Icon n={treeOpen?"arrow-l":"chev-r"} s={12}/>
              </button>
            </div>
            {treeOpen && (
              <>
                <div className="tree" style={{overflowY:"auto",flex:1}}>
                  {folderState.map(f => {
                    /* ✓ when a folder subtree has ≥1 WPID and every one is Done —
                       aggregation walks ALL descendants so nested folders count too */
                    const folderWpids = (id) => wpidsByFolder[id] || [];
                    const allDone = (list) => list.length > 0 && list.every(w => normalizeWpidStatus(w.status) === "done");
                    const subtreeDone = (node) => allDone(collectFolderIds(node).flatMap(folderWpids));
                    const doneMark = (
                      <span style={{color:"var(--ok-ink)",display:"inline-flex",flexShrink:0}} title="All WPIDs in this folder are Done">
                        <Icon n="check-circle" s={11}/>
                      </span>
                    );
                    /* Children render recursively — sub-folders nest with indent,
                       same as the SOP Template builder */
                    const renderKids = (kids, depth) => kids.map(c => (
                      <React.Fragment key={c.id}>
                        <div
                          className={"tree-node " + (c.id===activeFolder?"active":"")}
                          style={{
                            ...(depth > 0 ? {paddingLeft: 8 + depth * 16} : {}),
                            ...(dragOverFolder === c.id ? {background:"var(--accent-soft)",outline:"1px dashed var(--accent)"} : {}),
                          }}
                          draggable
                          onDragStart={(e)=>e.dataTransfer.setData("text/plain", JSON.stringify({ t:"folder", id: c.id }))}
                          {...dropProps(c.id)}
                          onClick={()=>{ setActiveFolder(c.id); setActiveWpid(null); setDetailOpen(false); }}
                          onContextMenu={(ev) => openCtx(ev, { type:"folder", id: c.id, name: c.name })}
                          title="Right-click for folder actions · drag to move">
                          {subtreeDone(c) ? doneMark : <Icon n={c.id===activeFolder?"folder-open":"folder"} s={12} c="muted"/>}
                          <span style={{flex:1,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>{c.name}</span>
                          <span className="mono" style={{fontSize:10,color:"var(--muted-2)"}}>{collectFolderIds(c).flatMap(folderWpids).length}</span>
                        </div>
                        {c.children && c.children.length > 0 && renderKids(c.children, depth + 1)}
                      </React.Fragment>
                    ));
                    return (
                    <div key={f.id}>
                      <div
                        className={"tree-node " + (f.open?"open":"") + (f.id===activeFolder?" active":"")}
                        style={dragOverFolder === f.id ? {background:"var(--accent-soft)",outline:"1px dashed var(--accent)"} : undefined}
                        draggable
                        onDragStart={(e)=>e.dataTransfer.setData("text/plain", JSON.stringify({ t:"folder", id: f.id }))}
                        {...dropProps(f.id)}
                        onClick={()=>{
                          toggleFolder(f.id);
                          /* childless top-level folders (e.g. 04 — AC, 05 — AR)
                             select directly so their WPIDs show in the table */
                          if (!f.children || f.children.length === 0) {
                            setActiveFolder(f.id); setActiveWpid(null); setDetailOpen(false);
                          }
                        }}
                        onContextMenu={(ev) => openCtx(ev, { type:"folder", id: f.id, name: f.name })}
                        title="Right-click for folder actions (Download ZIP, etc.)">
                        <span className="chev"><Icon n="chev" s={10}/></span>
                        {subtreeDone(f) ? doneMark : <Icon n={f.open?"folder-open":"folder"} s={14} c="muted"/>}
                        <span style={{flex:1,whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"}}>
                          {f.name}
                        </span>
                        <span className="mono" style={{fontSize:10,color:"var(--muted-2)"}}>{collectFolderIds(f).flatMap(folderWpids).length}</span>
                      </div>
                      {f.open && f.children && (
                        <div className="tree-kids">
                          {renderKids(f.children, 0)}
                        </div>
                      )}
                    </div>
                    );
                  })}
                </div>
              </>
            )}
          </div>

          {/* ── Middle: WPID list ─────────────────────────────── */}
          <div style={{display:"flex",flexDirection:"column",minWidth:0,overflow:"hidden"}}>
            {/* No context menu here — the visible ZIP/Add buttons already cover
               this header's actions; right-click lives on the tree (user
               decision 2026-06-12) */}
            <div style={{padding:"14px 20px 12px",borderBottom:"1px solid var(--line)",background:"#fff",flexShrink:0}}>
              <div style={{display:"flex",alignItems:"center",gap:10}}>
                <div>
                  <div style={{fontSize:15,fontWeight:600}}>{folderLabel}</div>
                  <div className="mono" style={{fontSize:11,color:"var(--muted)",marginTop:2}}>{folderSub}</div>
                </div>
                <div style={{marginLeft:"auto",display:"flex",gap:6,alignItems:"center"}}>
                  <button className="btn sm"
                    title="Download this folder as ZIP"
                    onClick={() => downloadZipMock({ type:"folder", id: activeFolder, name: folderLabel })}>
                    <Icon n="download" s={12}/> ZIP folder
                  </button>
                  {/* MTG3 §2.3 — Staff also have create/delete/edit rights on folders & WPIDs.
                     Upload + panel toggle were dropped here: selecting a file opens the
                     detail panel, which carries its own Upload and close controls. */}
                  <button className="btn sm" onClick={()=>setModal("addWpid")}
                    title="All roles (Admin/Supervisor/Staff) can add WPIDs">
                    <Icon n="plus" s={12}/> Add WPID
                  </button>
                </div>
              </div>
            </div>

            <div style={{padding:"12px 16px",overflowX:"auto",overflowY:"auto",flex:1}}>
              <div className="card">
                {currentWpids.length === 0 ? (
                  <EmptyState
                    icon="file"
                    title="No WPIDs in this folder"
                    sub="Add a WPID to start tracking a working paper."
                    cta={<button className="btn primary" onClick={()=>setModal("addWpid")}><Icon n="plus" s={13}/> Add WPID</button>}
                  />
                ) : filteredWpids.length === 0 ? (
                  <EmptyState
                    icon="search"
                    title="No WPIDs match the status filter"
                    sub="Try another status chip — or All to see everything in this folder."
                  />
                ) : (
                  <table className="tbl">
                    <thead><tr>
                      <th style={{width:32}}></th>
                      <SortTh label="File name" k="name" sort={wpSort} setSort={setWpSort}/>
                      <SortTh label="Status" k="status" sort={wpSort} setSort={setWpSort}/>
                      <SortTh label="Last updated" k="updated" sort={wpSort} setSort={setWpSort}/>
                      <th style={{width:32}}></th>
                    </tr></thead>
                    <tbody>
                      {filteredWpids.map(w => {
                        const wlock = lockInfo(w, CURRENT_ADMIN.id);
                        return (
                        <tr key={w.id}
                          draggable
                          onDragStart={(e)=>e.dataTransfer.setData("text/plain", JSON.stringify({ t:"wpid", id: w.id, from: activeFolder }))}
                          onClick={()=>{ setActiveWpid(w.id); setDetailOpen(true); setDetailTab("overview"); }}
                          onContextMenu={(ev) => openCtx(ev, { type:"wpid", id: w.id, name: w.name })}
                          style={{cursor:"pointer",background:w.id===activeWpid && detailOpen?"var(--accent-soft)":""}}>
                          <td><Icon n={fileIc(w.type)} s={14} c="muted"/></td>
                          <td>
                            <div style={{display:"flex",alignItems:"center",gap:6,fontWeight:500,lineHeight:1.3}}>
                              {w.name}
                              {wlock.locked && (
                                <span className="sbadge warn" style={{fontSize:9.5,padding:"1px 5px"}}
                                  title={`Locked by ${wlock.byName}${wlock.isMe ? " (you)" : ""}`}>
                                  <Icon n="lock" s={9}/> {wlock.isMe ? "You" : wlock.byName.split(" ")[0]}
                                </span>
                              )}
                              {!wlock.locked && w.draftBy && (
                                <span className="sbadge accent" style={{fontSize:9.5,padding:"1px 5px"}}
                                  title={`Unsaved web-edit draft by ${w.draftByName}`}>
                                  <Icon n="edit-3" s={9}/> Draft
                                </span>
                              )}
                            </div>
                            <div className="mono" style={{fontSize:10,color:"var(--muted-2)",marginTop:2}}>.{w.type} · {w.size}</div>
                          </td>
                          <td>{wpStatus(w.status)}</td>
                          <td>
                            {/* Files have no "owner" — what matters is who touched it last, and when */}
                            {(!w.owner || w.owner === "—") ? (
                              <span className="mono muted" style={{fontSize:11}}>—</span>
                            ) : (
                              <div style={{display:"flex",alignItems:"center",gap:7}}>
                                <div style={{width:22,height:22,borderRadius:"50%",background:"var(--panel-2)",border:"1px solid var(--line)",display:"grid",placeItems:"center",fontFamily:"var(--mono)",fontSize:9,fontWeight:600,color:"var(--ink-2)",flexShrink:0}}>
                                  {w.owner}
                                </div>
                                <div>
                                  <div style={{fontSize:11.5,lineHeight:1.2}}>{nameByInitials(w.owner)}</div>
                                  <div className="mono" style={{fontSize:10.5,color:"var(--muted)",marginTop:1}}>{w.updated}</div>
                                </div>
                              </div>
                            )}
                          </td>
                          <td><Icon n="more" s={14} c="muted"/></td>
                        </tr>
                        );
                      })}
                    </tbody>
                  </table>
                )}
              </div>
            </div>
          </div>

          {/* ── Right: WPID detail panel ──────────────────────── */}
          {wp && (
          <div style={{borderLeft:"1px solid var(--line)",background:"var(--panel-3)",display:"flex",flexDirection:"column",overflow:"hidden"}}>
            {/* Panel header */}
            <div style={{padding:"14px 16px 0",flexShrink:0}}>
              {/* Name first; status + lock/draft badges sit BELOW the meta line
                 (user request 2026-06-13) — actions stay top-right */}
              <div style={{display:"flex",alignItems:"flex-start",gap:8,marginBottom:2}}>
                {/* WPID code hidden — name over code (MTG3) */}
                <div style={{fontSize:14.5,fontWeight:600,lineHeight:1.3,flex:1,minWidth:0}}>{wp.name}</div>
                <div style={{display:"flex",gap:4,flexShrink:0}}>
                  <button className="btn ghost sm" style={{padding:"3px 6px"}} onClick={()=>setDetailOpen(false)}>
                    <Icon n="x" s={12}/>
                  </button>
                </div>
              </div>
              <div className="mono" style={{fontSize:11,color:"var(--muted)",marginBottom:8}}>
                {folderLabel} · .{wp.type} · {wp.size}
              </div>
              <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:12,flexWrap:"wrap"}}>
                {wpStatus(wp.status)}
                {(() => { const l = lockInfo(wp, CURRENT_ADMIN.id); return l.locked && (
                  <span className="sbadge warn" style={{fontSize:10}}
                    title={`Web Editor session by ${l.byName}${l.isMe ? " (you)" : ""}`}>
                    <Icon n="lock" s={10}/> {l.isMe ? "You're editing on web" : `Locked by ${l.byName}`}
                  </span>
                ); })()}
                {/* Dangling draft — closed editor, no version saved yet
                   (the Discard action lives in the button group below) */}
                {!lockInfo(wp, CURRENT_ADMIN.id).locked && wp.draftBy && (
                  <span className="sbadge accent" style={{fontSize:10}}
                    title={`Unsaved web-edit draft (based on ${wp.draftBaseV}) · ${wp.draftAt} — others can't web-edit until it's saved as a version or discarded; upload stays open`}>
                    <Icon n="edit-3" s={10}/> Draft by {wp.draftBy === CURRENT_ADMIN.id ? "you" : wp.draftByName}
                  </span>
                )}
              </div>

              {/* Detail tabs */}
              <div style={{display:"flex",gap:2,borderBottom:"1px solid var(--line)",marginLeft:-16,marginRight:-16,paddingLeft:16}}>
                {(() => {
                  const sheets = sheetsByWpid[wp.id] || {};
                  const openQA = Object.values(sheets).reduce((sum, rounds) => sum + rounds.filter(r => !r.a).length, 0);
                  return [
                    {id:"overview", label:"Overview"},
                    {id:"task",     label:"Review Sheets", badge: openQA || null},
                    {id:"signoff",  label:"Sign-off", badge: chain.filter(c=>c.status==="pending" && c.required).length || null},
                  ];
                })().map(t => (
                  <button key={t.id}
                    onClick={()=>setDetailTab(t.id)}
                    style={{
                      padding:"5px 10px",
                      fontSize:11.5,
                      fontWeight: detailTab===t.id ? 600 : 400,
                      color: detailTab===t.id ? "var(--ink)" : "var(--muted)",
                      background:"none",border:"none",cursor:"pointer",
                      borderBottom: detailTab===t.id ? "2px solid var(--ink)" : "2px solid transparent",
                      marginBottom:-1,
                      display:"flex",alignItems:"center",gap:4,
                    }}>
                    {t.label}
                    {t.badge > 0 && (
                      <span style={{fontSize:9,background:"var(--warn-soft)",color:"var(--warn-ink)",padding:"1px 4px",borderRadius:10,fontFamily:"var(--mono)",fontWeight:700}}>
                        {t.badge}
                      </span>
                    )}
                  </button>
                ))}
              </div>
            </div>

            {/* Detail tab content */}
            <div style={{flex:1,overflowY:"auto",padding:"14px 16px"}}>

              {/* ── Overview tab ── */}
              {detailTab === "overview" && (
                <>
                  {/* File block — flat, no nested boxes (user decision 2026-06-12):
                     a plain prepared-by line + one button row */}
                  {wp.v === "—" && (
                    <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:10}}>
                      <div style={{width:40,height:40,borderRadius:6,background:"var(--panel-2)",display:"grid",placeItems:"center",color:"var(--muted)"}}>
                        <Icon n={fileIc(wp.type)} s={20}/>
                      </div>
                      <div style={{flex:1,minWidth:0}}>
                        <div style={{fontSize:12.5,fontWeight:500,lineHeight:1.3}}>
                          <span className="muted">No file uploaded yet</span>
                        </div>
                        <div className="mono" style={{fontSize:10.5,color:"var(--muted)",marginTop:2}}>
                          Awaiting first upload
                        </div>
                      </div>
                    </div>
                  )}
                  {wp.preparedBy && wp.v !== "—" && (
                    <div style={{marginBottom:8,fontSize:11,display:"flex",flexDirection:"column",gap:6}}>
                      {/* Created = first upload (MTG4 prepared-by auto-stamp) */}
                      <div>
                        <div style={{display:"flex",alignItems:"center",gap:6}}>
                          <Icon n="pencil" s={11} c="muted"/>
                          <span className="muted">Prepared by</span>
                          <b>{wp.preparedByName}</b>
                        </div>
                        <div className="mono muted" style={{fontSize:10,marginTop:2,paddingLeft:17}}>{fmtTsShort(wp.preparedAt)}</div>
                      </div>
                      {/* Most recent touch — who pushed the current version */}
                      {wp.updated && wp.updated !== "—" && (
                        <div>
                          <div style={{display:"flex",alignItems:"center",gap:6}}>
                            <Icon n="history" s={11} c="muted"/>
                            <span className="muted">Last updated by</span>
                            <b>{nameByInitials(wp.owner) || wp.owner}</b>
                          </div>
                          <div className="mono muted" style={{fontSize:10,marginTop:2,paddingLeft:17}}>{wp.updated}</div>
                        </div>
                      )}
                    </div>
                  )}
                  {/* Actions grouped by concern, stacked top→bottom by frequency
                     (user request 2026-06-13): Preview (most used, most
                     prominent) → file in/out → web editing (+Discard when a
                     draft dangles) → Delete */}
                  {(() => {
                    const l = lockInfo(wp, CURRENT_ADMIN.id);
                    const editable = isWebEditable(wp.type);
                    const blocked = l.locked && !l.isMe;
                    const draftOther = wp.draftBy && wp.draftBy !== CURRENT_ADMIN.id;
                    const draftMine  = wp.draftBy === CURRENT_ADMIN.id;
                    return (
                  <div style={{display:"flex",flexDirection:"column",gap:6,marginBottom:14}}>
                    {/* 1 — Preview */}
                    <button className="btn primary" style={{width:"100%",justifyContent:"center"}}
                      disabled={wp.v === "—"}
                      title={wp.v === "—" ? "No file to preview yet" : "Preview current version"}
                      onClick={()=>setModal(hardLocked ? "previewReason" : "preview")}>
                      <Icon n="eye" s={13}/> Preview
                    </button>
                    {/* 2 — file in/out. Download works even while locked (everyone
                       sees `current`); upload is blocked while ANY web-edit session
                       is open, including your own (a draft and an upload would race
                       to become current — user decision 2026-06-12) */}
                    <div style={{display:"flex",gap:6}}>
                      <button className="btn sm" style={{flex:1,justifyContent:"center"}} disabled={wp.v === "—"}
                        title={wp.v === "—" ? "No file to download yet" : "Download current version"}
                        onClick={()=>{
                          nav.toast(`Download started — ${wp.name} (${wp.v})`, "ok");
                          nav.log && nav.log({ action:"download", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:`Downloaded ${wp.v} from ${folderPathById(activeFolder)}` });
                        }}>
                        <Icon n="download" s={12}/> Download
                      </button>
                      <button className="btn sm" style={{flex:1,justifyContent:"center"}}
                        disabled={l.locked}
                        title={!l.locked ? "Upload new version"
                          : l.isMe ? "Upload disabled — finish or discard your web edit first"
                          : `Upload disabled — locked by ${l.byName}`}
                        onClick={()=>setModal("upload")}>
                        <Icon n="upload" s={12}/> Upload
                      </button>
                    </div>
                    {/* 3 — web editing; Discard joins the group when a draft dangles */}
                    {editable && (
                      <div style={{display:"flex",gap:6}}>
                        <button className="btn sm accent" style={{flex:1,justifyContent:"center"}}
                          disabled={blocked || draftOther}
                          title={blocked ? `Locked by ${l.byName}`
                            : draftOther ? `Draft by ${wp.draftByName} — only they can continue (Admin can discard the draft)`
                            : draftMine ? "Continue your unsaved draft"
                            : "Edit Word/Excel directly in browser"}
                          onClick={()=>handleOpenWebEditor(wp)}>
                          <Icon n="edit-3" s={12}/> {(l.isMe || draftMine) ? "Continue" : "Edit on web"}
                        </button>
                        {wp.draftBy && !l.locked && (
                          <button className="btn sm" style={{flex:1,justifyContent:"center"}}
                            title={draftMine ? "Discard your draft" : "Discard this draft (Admin) — logged as critical"}
                            onClick={handleDiscardDraft}>
                            <Icon n="x" s={12}/> Discard draft
                          </button>
                        )}
                      </div>
                    )}
                    {/* 4 — delete */}
                    <button className="btn sm" style={{width:"100%",justifyContent:"center",color:"var(--danger-ink)"}}
                      title="Delete WPID — moves to Recycle Bin"
                      onClick={()=>setModal("deleteWpid")}>
                      <Icon n="trash" s={12}/> Delete
                    </button>
                  </div>
                    );
                  })()}

                  {/* Status transition — 6-state lifecycle (Proud Workpapers PDF slide 5).
                     Bare buttons, no framing box (user decision 2026-06-12) */}
                  {(() => {
                    const curStatus = normalizeWpidStatus(wp.status);
                    return (
                      <div style={{marginBottom:14}}>
                        {curStatus==="in-progress" && (
                          <button className="btn sm primary" style={{width:"100%",justifyContent:"center"}}
                            title="Submit → Waiting for Review" onClick={()=>setModal("submitReview")}>
                            <Icon n="send" s={11}/> Submit
                          </button>
                        )}

                        {curStatus==="waiting-review" && (
                          <div style={{display:"flex",flexDirection:"column",gap:6}}>
                            <button className="btn sm danger" style={{width:"100%",justifyContent:"center"}} onClick={handleAssignReviewNotes}>
                              <Icon n="edit-3" s={11}/> Assign Review Notes
                            </button>
                            <button className="btn sm accent" style={{width:"100%",justifyContent:"center"}}
                              onClick={handleAcceptResponse}
                              disabled={!allRequiredDone}
                              title={!allRequiredDone ? "All required signers must sign first" : "Accept → Completed"}>
                              <Icon n="check-circle" s={11}/> Accept → Completed
                            </button>
                            <button className="btn sm" style={{width:"100%",justifyContent:"center"}} onClick={handleBackToInProgress}>
                              <Icon n="pencil" s={11}/> ← Back to In Progress
                            </button>
                          </div>
                        )}

                        {curStatus==="rn-assigned" && (
                          <div style={{display:"flex",flexDirection:"column",gap:6}}>
                            <button className="btn sm accent" style={{width:"100%",justifyContent:"center"}} onClick={handleRespondReviewNotes}>
                              <Icon n="send" s={11}/> Submit Response → Responded
                            </button>
                            <button className="btn sm" style={{width:"100%",justifyContent:"center"}} onClick={handleBackToInProgress}>
                              <Icon n="pencil" s={11}/> ← Back to In Progress
                            </button>
                          </div>
                        )}

                        {curStatus==="rn-responded" && (
                          <div style={{display:"flex",flexDirection:"column",gap:6}}>
                            <button className="btn sm accent" style={{width:"100%",justifyContent:"center"}}
                              onClick={handleAcceptResponse}
                              disabled={!allRequiredDone || openNotes.length > 0}
                              title={
                                openNotes.length > 0 ? "Resolve open notes first" :
                                !allRequiredDone ? "All required signers must sign first" :
                                "All checks pass — mark Completed"
                              }>
                              <Icon n="check-circle" s={11}/> Accept → Completed
                            </button>
                            <button className="btn sm danger" style={{width:"100%",justifyContent:"center"}} onClick={handleRejectResponse}>
                              <Icon n="x-circle" s={11}/> Reject → Rejected
                            </button>
                          </div>
                        )}

                        {curStatus==="rn-rejected" && (
                          <button className="btn sm primary" style={{width:"100%",justifyContent:"center"}} onClick={handleBackToInProgress}>
                            <Icon n="pencil" s={11}/> Rework → In Progress
                          </button>
                        )}

                        {curStatus==="completed" && (
                          <div style={{display:"flex",alignItems:"center",gap:6,padding:"6px 0",color:"var(--ok-ink)",fontSize:11.5}}>
                            <Icon n="check-circle" s={12}/>
                            <span>WPID Completed. Required signers all done · review notes resolved.</span>
                          </div>
                        )}
                      </div>
                    );
                  })()}

                  {/* Version history */}
                  <div style={{height:1,background:"var(--line)",margin:"0 0 12px"}}/>
                  <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:8}}>
                    <div className="mono" style={{fontSize:10,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".06em"}}>
                      Version history
                    </div>
                    {versions.length >= 2 && (
                      <button className="btn ghost sm" onClick={()=>setModal("diff")}>
                        <Icon n="history" s={11}/> Compare
                      </button>
                    )}
                  </div>
                  {versions.length === 0 ? (
                    <div className="mono" style={{fontSize:11,color:"var(--muted-2)",padding:"4px 0"}}>No versions yet — upload to begin tracking.</div>
                  ) : (
                    versions.map((h,i) => (
                      <div key={i} style={{display:"flex",gap:10,padding:"8px 0",borderBottom:i<versions.length-1?"1px solid var(--line)":"0"}}>
                        <div style={{width:22,height:22,borderRadius:"50%",background:"var(--panel-2)",border:"1px solid var(--line)",display:"grid",placeItems:"center",fontFamily:"var(--mono)",fontSize:9,fontWeight:600,color:"var(--ink-2)",flexShrink:0}}>
                          {h.who}
                        </div>
                        <div style={{flex:1,minWidth:0}}>
                          <div style={{display:"flex",alignItems:"center",gap:6}}>
                            <span className="mono" style={{fontSize:11,fontWeight:600}}>{h.v}</span>
                            <span className="mono" style={{fontSize:10,color:"var(--muted-2)"}}> · {h.t}</span>
                          </div>
                          <div style={{fontSize:11.5,color:"var(--ink-2)",marginTop:2}}>{h.note}</div>
                        </div>
                        {!h.v.includes("(current)") && (
                          <button className="btn ghost sm" style={{padding:"2px 4px"}}
                            title={`Restore ${h.v} — creates a new version copy (history is kept)`}
                            onClick={()=>setModal({ kind:"restoreVersion", entry: h })}>
                            <Icon n="refresh" s={12}/>
                          </button>
                        )}
                      </div>
                    ))
                  )}
                </>
              )}

              {/* ── Sign-off tab ── */}
              {detailTab === "signoff" && (
                <ReviewerSlotsPanel chain={chain} activeVersion={wp.v}
                  postT0={!!project.auditorReportDate}
                  onSign={handleSign}
                  onCancel={handleCancelSign}
                  onAddSlot={handleAddSlot}
                  onConfigure={() => setModal("signConfig")}
                  onOverrideTimestamp={(idx) => setModal({ kind:"tsOverride", idx })}/>
              )}

              {/* ── Review Sheets tab (was Task / Review Notes) ── */}
              {detailTab === "task" && (
                <ReviewSheetsPanel chain={chain}
                  sheetsBySlot={sheetsByWpid[wp.id] || {}}
                  expandedSlot={expandedSheetSlot}
                  setExpandedSlot={setExpandedSheetSlot}
                  onAskQuestion={handleAskQuestion}
                  onAnswerQuestion={handleAnswerQuestion}/>
              )}

            </div>
          </div>
          )}
        </div>
      )}

      {activeTab === "tasks" && (
        <KanbanBoard
          wpidsByFolder={wpidsByFolder}
          notesByWpid={notesByWpid}
          folderTree={PROJECT_STRUCTURE}
          onOpenWpid={(folderId, wpidId) => {
            setActiveTab("structure");
            setActiveFolder(folderId);
            setActiveWpid(wpidId);
            setDetailOpen(true);
            setDetailTab("overview");
          }}
        />
      )}

      {activeTab === "team" && (() => {
        /* Live team — derived from project.members + creator (stays in sync with
           the Settings tab), with each member's reviewer slot(s) from
           project.reviewerSlots. Replaces the old static PROJECT_TEAM table. */
        const memberIds = [...new Set([project.createdBy, ...(project.members || [])])].filter(Boolean);
        /* Same fallback as the Settings tab so both views describe the same chain */
        const slots = (project.reviewerSlots && project.reviewerSlots.length ? project.reviewerSlots : DEFAULT_REVIEWER_SLOTS);
        const slotsFor = (uid) => slots.filter(r => r.userId === uid);
        const unassigned = slots.filter(r => !r.userId);
        return (
          <div className="page-body">
            <div className="sec">
              <div className="sec-head">
                <div className="sec-title"><Icon n="users" s={15}/> Project Team <span className="badge">{memberIds.length}</span></div>
              </div>
              <div className="sec-body">
                <table className="tbl">
                  <thead><tr><th>Member</th><th>Role</th><th>Reviewer Role</th></tr></thead>
                  <tbody>
                    {memberIds.map(uid => {
                      const u = userById(uid);
                      const mySlots = slotsFor(uid);
                      return (
                        <tr key={uid}>
                          <td>
                            <div style={{display:"flex",alignItems:"center",gap:10}}>
                              <Avatar id={uid} name={u.name} size="sm"/>
                              <div>
                                <div style={{fontWeight:500}}>{u.name}</div>
                                <div className="mono muted" style={{fontSize:10.5}}>{u.email}</div>
                              </div>
                            </div>
                          </td>
                          <td style={{color:"var(--muted)"}}>{u.role}</td>
                          <td>
                            {mySlots.length === 0
                              ? <span className="muted" style={{fontSize:12}}>—</span>
                              : (
                                <div style={{display:"flex",gap:6,flexWrap:"wrap"}}>
                                  {mySlots.map((r, i) => (
                                    <span key={i} className={`sbadge ${r.required ? "warn" : ""}`} style={{fontSize:11}}
                                      title={r.required ? "Required — must sign before a WPID becomes Done" : "Optional — can sign but doesn't block"}>
                                      {r.role}{r.required ? " · Required" : ""}
                                    </span>
                                  ))}
                                </div>
                              )}
                          </td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
                {unassigned.length > 0 && (
                  <div className="help" style={{marginTop:10,display:"flex",alignItems:"center",gap:6,flexWrap:"wrap"}}>
                    <Icon n="alert" s={12}/>
                    <span>Unassigned reviewer role{unassigned.length !== 1 ? "s" : ""}:</span>
                    {unassigned.map((r, i) => (
                      <span key={i} className="sbadge draft" style={{fontSize:11}}>{r.role}{r.required ? " · Required" : ""}</span>
                    ))}
                    <span>— assign in <a onClick={()=>setActiveTab("settings")} style={{cursor:"pointer",textDecoration:"underline"}}>Settings</a></span>
                  </div>
                )}
              </div>
            </div>
          </div>
        );
      })()}

      {activeTab === "activity" && (
        <ActivityScreen state={state} nav={nav} projectId={projectId} embedded/>
      )}

      {activeTab === "recycle" && (
        <RecycleBinScreen state={state} nav={nav} scopedProjectId={projectId} embedded/>
      )}

      {activeTab === "settings" && (
        <ProjectSettingsPanel project={project} entity={entity} nav={nav}
          onZip={() => downloadZipMock({ type:"project", id: projectId, name: project.name })}/>
      )}

      {/* Modals — wpid-scoped */}
      {modal === "preview" && wp && (
        <PreviewModal wpid={wp} folderLabel={folderLabel} onClose={()=>setModal(null)}
          onEditOnWeb={(t) => { setModal(null); handleOpenWebEditor(t); }}/>
      )}
      {modal === "webEdit" && wp && (
        <WebEditorModal wpid={wp} folderLabel={folderLabel}
          onClose={handleWebEditClose}
          onSave={handleWebEditSave}/>
      )}
      {modal === "upload" && wp && (
        <UploadModal wpid={wp} folderLabel={folderLabel} onClose={()=>setModal(null)} onConfirm={handleUpload}/>
      )}
      {modal === "addNote" && wp && (
        <AddNoteModal wpid={wp} allWpids={currentWpids} onClose={()=>setModal(null)} onSave={handleAddNote}/>
      )}
      {modal === "setReportDate" && (
        <SetReportDateModal project={project}
          requiredSignedReady={requiredSignedReady}
          onClose={()=>setModal(null)}
          onSave={handleSetReportDate}/>
      )}
      {modal === "previewReason" && wp && (
        <ArchiveViewReasonModal wpid={wp}
          onClose={()=>setModal(null)}
          onConfirm={(reason) => {
            nav.log && nav.log({
              action:"view", scope:projectId,
              obj:`WP-${wp.id} ${wp.name}`,
              detail:`Archived-file open · reason: "${reason}"`,
              severity:"critical",
            });
            nav.toast("Reason logged — opening file", "ok");
            setModal("preview");
          }}/>
      )}
      {modal === "addFolder" && (
        <AddFolderModalP
          sections={flattenFolders(folderState)}
          defaultParentId={activeFolder}
          onClose={()=>setModal(null)}
          onSave={(payload)=>{ if (handleAddFolder(payload)) setModal(null); }}/>
      )}
      {modal === "addWpid" && (
        <AddWpidModalP folderName={folderLabel} onClose={()=>setModal(null)} onSave={handleAddWpid}/>
      )}
      {modal && typeof modal === "object" && modal.kind === "moveWpid" && (
        <MoveTargetModal title="Move WPID" itemName={modal.name}
          sections={flattenFolders(folderState).filter(s => s.id !== modal.from)}
          onClose={()=>setModal(null)}
          onMove={(target)=>{ setModal(null); moveWpidTo(modal.id, modal.from, target); }}/>
      )}
      {modal && typeof modal === "object" && modal.kind === "moveFolder" && (() => {
        const node = findFolderNode(folderState, modal.id);
        const excluded = node ? collectFolderIds(node) : [modal.id];
        const sections = [
          { id:"__root__", name:"(Top level)", depth:0 },
          ...flattenFolders(folderState).filter(s => !excluded.includes(s.id)),
        ];
        return (
          <MoveTargetModal title="Move folder" itemName={modal.name} sections={sections}
            onClose={()=>setModal(null)}
            onMove={(target)=>{ setModal(null); moveFolderTo(modal.id, target); }}/>
        );
      })()}
      {modal && typeof modal === "object" && modal.kind === "restoreVersion" && wp && (
        <RestoreVersionModal wp={wp} entry={modal.entry}
          onClose={()=>setModal(null)}
          onConfirm={()=>{ const h = modal.entry; setModal(null); handleRestoreVersion(h); }}/>
      )}
      {modal && typeof modal === "object" && modal.kind === "staleDraft" && wp && (
        <StaleDraftModal wp={wp}
          onClose={()=>setModal(null)}
          onDiscard={handleDiscardDraft}
          onContinue={()=>{ setModal(null); openEditorNow(wp); }}/>
      )}
      {modal === "submitReview" && wp && (
        <SubmitReviewModal wp={wp} chain={chain}
          onClose={()=>setModal(null)}
          onConfirm={()=>{ setModal(null); handleSubmitForReview(); }}/>
      )}
      {modal === "deleteWpid" && wp && (
        <DeleteWpidModal wpid={wp} onClose={()=>setModal(null)} onConfirm={handleDeleteWpid}/>
      )}
      {modal === "diff" && wp && versions.length >= 2 && (
        <DiffModal wpid={wp} versions={versions} onClose={()=>setModal(null)}/>
      )}
      {modal === "signConfig" && wp && (
        <SignChainConfigModal chain={chain} onClose={()=>setModal(null)}
          onSave={(newConfig)=>{
            setSignChainByWpid(prev => ({...prev, [wp.id]: newConfig}));
            nav.toast("Sign-off configuration saved", "ok");
            nav.log && nav.log({ action:"perm", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:"Sign-off chain Required/Optional updated", severity:"critical" });
            setModal(null);
          }}/>
      )}
      {modal && typeof modal === "object" && modal.kind === "tsOverride" && wp && (
        <TimestampOverrideModal entry={chain[modal.idx]} onClose={()=>setModal(null)}
          onSave={({ts})=>{
            setSignChainByWpid(prev => ({
              ...prev,
              [wp.id]: (prev[wp.id] || chain).map((c, i) => i === modal.idx
                ? { ...c, signedAt: fmtTs(ts), signedAtIso: ts, overrideBy: CURRENT_ADMIN.id }
                : c),
            }));
            nav.toast("Timestamp override saved", "ok");
            nav.log && nav.log({ action:"perm", scope:projectId, obj:`WP-${wp.id} ${wp.name}`, detail:`Sign-off timestamp override · ${chain[modal.idx].role}`, severity:"critical" });
            setModal(null);
          }}/>
      )}

      {/* Right-click context menu — Download ZIP per MTG3 §2.4 */}
      {ctxMenu && (
        <div
          style={{
            position:"fixed",
            left: Math.min(ctxMenu.x, window.innerWidth - 240),
            top: Math.min(ctxMenu.y, window.innerHeight - 200),
            background:"#fff",
            border:"1px solid var(--line)",
            borderRadius:"var(--radius)",
            boxShadow:"0 8px 24px rgba(0,0,0,.12)",
            minWidth:220,
            zIndex:2000,
            overflow:"hidden",
          }}
          onClick={(e)=>e.stopPropagation()}
        >
          <div style={{padding:"8px 12px",borderBottom:"1px solid var(--line)",background:"var(--panel-3)"}}>
            <div className="mono" style={{fontSize:9.5,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".05em"}}>
              {ctxMenu.target.type}
            </div>
            <div style={{fontWeight:500,fontSize:12,marginTop:2}} className="truncate">{ctxMenu.target.name}</div>
          </div>
          {/* ZIP makes sense for containers; a single WPID gets plain
             Preview + Download instead (user request 2026-06-13) */}
          {ctxMenu.target.type !== "wpid" && (
            <button className="ctx-item" onClick={() => downloadZipMock(ctxMenu.target)}
              style={{display:"flex",alignItems:"center",gap:8,padding:"9px 12px",background:"none",border:"none",width:"100%",cursor:"pointer",fontSize:12.5,textAlign:"left"}}>
              <Icon n="download" s={13} c="muted"/>
              <span>Download {ctxMenu.target.type === "project" ? "project" : "folder"} as ZIP</span>
            </button>
          )}
          {ctxMenu.target.type === "wpid" && (() => {
            const t = currentWpids.find(w => w.id === ctxMenu.target.id);
            const noFile = !t || t.v === "—";
            const itemStyle = {display:"flex",alignItems:"center",gap:8,padding:"9px 12px",background:"none",border:"none",width:"100%",cursor:noFile?"not-allowed":"pointer",fontSize:12.5,textAlign:"left",opacity:noFile?0.45:1};
            return (
              <>
                <button className="ctx-item" disabled={noFile} style={itemStyle}
                  title={noFile ? "No file uploaded yet" : ""}
                  onClick={() => {
                    setCtxMenu(null);
                    setActiveWpid(t.id); setDetailOpen(true); setDetailTab("overview");
                    setModal(hardLocked ? "previewReason" : "preview");
                  }}>
                  <Icon n="eye" s={13} c="muted"/>
                  <span>Preview file</span>
                </button>
                <button className="ctx-item" disabled={noFile} style={itemStyle}
                  title={noFile ? "No file uploaded yet" : ""}
                  onClick={() => {
                    setCtxMenu(null);
                    nav.toast(`Download started — ${t.name} (${t.v})`, "ok");
                    nav.log && nav.log({ action:"download", scope:projectId, obj:`WP-${t.id} ${t.name}`, detail:`Downloaded ${t.v} from ${folderPathById(activeFolder)}` });
                  }}>
                  <Icon n="download" s={13} c="muted"/>
                  <span>Download file</span>
                </button>
              </>
            );
          })()}
          {ctxMenu.target.type === "folder" && (
            <>
              <button className="ctx-item"
                onClick={() => {
                  /* target the right-clicked folder, not whatever was active */
                  setCtxMenu(null); setActiveFolder(ctxMenu.target.id);
                  setActiveWpid(null); setDetailOpen(false);
                  setModal("addWpid");
                }}
                style={{display:"flex",alignItems:"center",gap:8,padding:"9px 12px",background:"none",border:"none",width:"100%",cursor:"pointer",fontSize:12.5,textAlign:"left"}}>
                <Icon n="plus" s={13} c="muted"/>
                <span>Add WPID here</span>
              </button>
              <button className="ctx-item"
                onClick={() => {
                  setCtxMenu(null); setActiveFolder(ctxMenu.target.id);
                  setActiveWpid(null); setDetailOpen(false);
                  setModal("addFolder");
                }}
                style={{display:"flex",alignItems:"center",gap:8,padding:"9px 12px",background:"none",border:"none",width:"100%",cursor:"pointer",fontSize:12.5,textAlign:"left"}}>
                <Icon n="folder" s={13} c="muted"/>
                <span>Add sub-folder here</span>
              </button>
              <button className="ctx-item"
                onClick={() => { setCtxMenu(null); setModal({ kind:"moveFolder", id: ctxMenu.target.id, name: ctxMenu.target.name }); }}
                style={{display:"flex",alignItems:"center",gap:8,padding:"9px 12px",background:"none",border:"none",width:"100%",cursor:"pointer",fontSize:12.5,textAlign:"left"}}>
                <Icon n="arrow-r" s={13} c="muted"/>
                <span>Move folder…</span>
              </button>
            </>
          )}
          {ctxMenu.target.type === "wpid" && (
            <button className="ctx-item"
              onClick={() => { setCtxMenu(null); setModal({ kind:"moveWpid", id: ctxMenu.target.id, name: ctxMenu.target.name, from: activeFolder }); }}
              style={{display:"flex",alignItems:"center",gap:8,padding:"9px 12px",background:"none",border:"none",width:"100%",cursor:"pointer",fontSize:12.5,textAlign:"left"}}>
              <Icon n="arrow-r" s={13} c="muted"/>
              <span>Move to folder…</span>
            </button>
          )}
          {ctxMenu.target.type === "wpid" && (
            <button className="ctx-item" onClick={() => { setCtxMenu(null); setActiveWpid(ctxMenu.target.id); setModal("deleteWpid"); }}
              style={{display:"flex",alignItems:"center",gap:8,padding:"9px 12px",background:"none",border:"none",width:"100%",cursor:"pointer",fontSize:12.5,textAlign:"left",color:"var(--danger-ink)"}}>
              <Icon n="trash" s={13}/>
              <span>Move to Recycle Bin</span>
            </button>
          )}
        </div>
      )}
    </>
  );
};
