/* =============================================================
   components.jsx — shared UI primitives
   ============================================================= */
const { useMemo, useState, useEffect, useRef, useCallback } = React;

function Glyph({ name, size = 16 }) {
  // Tiny inline SVG icons. Stroke = currentColor.
  const paths = {
    overview: <><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></>,
    map: <><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></>,
    underlay: <><path d="M3 18h18M3 12h18M3 6h18"/></>,
    overlay: <><path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"/></>,
    risk: <><path d="M12 3 2 21h20L12 3z"/><path d="M12 10v5M12 18v.5"/></>,
    sites: <><path d="M12 22s7-6 7-12a7 7 0 1 0-14 0c0 6 7 12 7 12z"/><circle cx="12" cy="10" r="2.5"/></>,
    batch: <><rect x="3" y="6" width="18" height="4" rx="1"/><rect x="3" y="14" width="14" height="4" rx="1"/></>,
    chat: <><path d="M21 12a8 8 0 0 1-8 8H6l-3 2v-2a8 8 0 1 1 18-8z"/></>,
    refresh: <><path d="M3 12a9 9 0 0 1 15.5-6.3M21 3v6h-6"/><path d="M21 12a9 9 0 0 1-15.5 6.3M3 21v-6h6"/></>,
    search: <><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></>,
    close: <><path d="M6 6l12 12M18 6L6 18"/></>,
    chevron: <><path d="M9 6l6 6-6 6"/></>,
    arrowUp: <><path d="M12 19V5M5 12l7-7 7 7"/></>,
    arrowDown: <><path d="M12 5v14M5 12l7 7 7-7"/></>,
    ext: <><path d="M14 4h6v6M10 14L20 4M20 14v6H4V4h6"/></>,
    send: <><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></>,
    file: <><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/></>,
    info: <><circle cx="12" cy="12" r="9"/><path d="M12 8h.01M11 12h1v5h1"/></>,
    check: <><path d="M5 12l4 4 10-10"/></>,
    filter: <><path d="M3 5h18M6 12h12M10 19h4"/></>,
    pm: <><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M7 8h10M7 12h10M7 16h6"/></>,
    edit: <><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></>,
    add: <><path d="M12 5v14M5 12h14"/></>,
    drag: <><circle cx="9" cy="6" r="1.4"/><circle cx="15" cy="6" r="1.4"/><circle cx="9" cy="12" r="1.4"/><circle cx="15" cy="12" r="1.4"/><circle cx="9" cy="18" r="1.4"/><circle cx="15" cy="18" r="1.4"/></>,
  };
  const p = paths[name];
  if (!p) return null;
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
      {p}
    </svg>
  );
}

/* ---------- KPI ---------- */
function KPI({ label, value, unit, delta, accent, hint }) {
  return (
    <div className={"kpi" + (accent ? " accent" : "")}>
      <div className="label">{label}</div>
      <div className="value">
        {value}
        {unit && <span className="unit">{unit}</span>}
      </div>
      {delta !== undefined && (
        <div className="delta">
          {hint && <span>{hint}</span>}
          {delta !== null && <span className="pct">{delta}</span>}
        </div>
      )}
    </div>
  );
}

/* ---------- Pill ---------- */
function Pill({ kind = "muted", children }) {
  return (
    <span className={"pill " + kind}>
      <span className="dot" />
      <span>{children}</span>
    </span>
  );
}

function RelationshipWarningCell({ row }) {
  const rawWarnings = row && (row._relationshipWarnings || row.relationshipWarnings || []);
  const warnings = Array.isArray(rawWarnings) ? rawWarnings : [];
  if (!warnings.length) return <span className="muted">—</span>;
  const title = warnings.map((w) => {
    const source = w.sourceSheet ? ` (${w.sourceSheet}${w.sourceRowNumber ? " row " + w.sourceRowNumber : ""})` : "";
    return `${w.message || w.code}${source}`;
  }).join("\n");
  return (
    <span title={title}>
      <Pill kind="warn">{warnings.length} warning{warnings.length === 1 ? "" : "s"}</Pill>
    </span>
  );
}

function CriticalityPill({ value }) {
  const v = (value || "").toLowerCase();
  if (v === "gold") return <Pill kind="gold">Gold</Pill>;
  if (v === "silver") return <Pill kind="silver">Silver</Pill>;
  if (v === "bronze") return <Pill kind="bronze">Bronze</Pill>;
  return <Pill kind="muted">—</Pill>;
}

function StatusPill({ value, kind }) {
  if (!value) return <span className="muted" style={{fontSize:12}}>—</span>;
  const bucket = kind ? PRData.statusBucket(value, kind) : "muted";
  return <Pill kind={bucket}>{value}</Pill>;
}

function RiskPill({ level, atRisk }) {
  if (atRisk && atRisk.toLowerCase() === "yes") {
    if (level && level.toLowerCase() === "high") return <Pill kind="risk">High Risk</Pill>;
    if (level && level.toLowerCase() === "medium") return <Pill kind="warn">Medium Risk</Pill>;
    return <Pill kind="warn">At Risk</Pill>;
  }
  if (level && level.toLowerCase() === "low") return <Pill kind="info">Low</Pill>;
  if (level && level.toLowerCase() === "high") return <Pill kind="risk">High</Pill>;
  if (level && level.toLowerCase() === "medium") return <Pill kind="warn">Medium</Pill>;
  return <Pill kind="muted">—</Pill>;
}

/* ---------- Progress ---------- */
function Progress({ value, max = 100, kind, thin, thick }) {
  const pct = max > 0 ? Math.max(0, Math.min(100, (value / max) * 100)) : 0;
  return (
    <div className={"progress" + (thin ? " thin" : "") + (thick ? " thick" : "") + (kind ? " " + kind : "")}>
      <span style={{ width: pct + "%" }} />
    </div>
  );
}

/* ---------- Empty / Loading ---------- */
function Empty({ title = "Nothing here yet", sub = "" }) {
  return (
    <div className="empty-state">
      <div className="glyph"><Glyph name="info" size={18}/></div>
      <div className="ttl">{title}</div>
      {sub && <div className="sub">{sub}</div>}
    </div>
  );
}

function Skeleton({ width = "100%", height = 14 }) {
  return <div className="skel" style={{ width, height }} />;
}

/* ---------- Filter chip / search ---------- */
function FilterSelect({ label, value, onChange, options }) {
  return (
    <span className="filter-chip">
      <label>{label}</label>
      <select value={value || ""} onChange={(e) => onChange(e.target.value)}>
        <option value="">All</option>
        {options.map((o) => (
          <option key={o.value || o} value={o.value || o}>{o.label || o}</option>
        ))}
      </select>
    </span>
  );
}

function SearchInput({ value, onChange, placeholder = "Search…" }) {
  return (
    <div className="search-input">
      <Glyph name="search" size={14} />
      <input value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder} />
    </div>
  );
}

/* ---------- Toast ---------- */
function Toast({ msg, show }) {
  return <div className={"toast" + (show ? " show" : "")}>{msg}</div>;
}

/* ---------- Page header ---------- */
function PageHeader({ title, subtitle, right }) {
  return (
    <div className="page-header">
      <div>
        <h1 className="page-title">{title}</h1>
        {subtitle && <p className="page-subtitle">{subtitle}</p>}
      </div>
      {right && <div>{right}</div>}
    </div>
  );
}

/* ---------- Tabs ---------- */
function Tabs({ value, onChange, options }) {
  return (
    <div className="tabs">
      {options.map((o) => (
        <button key={o.value} className={value === o.value ? "active" : ""} onClick={() => onChange(o.value)}>
          {o.label}
          {o.count !== undefined && <span style={{marginLeft:6, color: "var(--ink-400)", fontFamily: "var(--font-mono)", fontSize: 11}}>{o.count}</span>}
        </button>
      ))}
    </div>
  );
}

/* ---------- Side panel ---------- */
function SidePanel({ open, onClose, title, subtitle, children, className = "" }) {
  useEffect(() => {
    function onKey(e) { if (e.key === "Escape") onClose && onClose(); }
    if (open) window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);
  return (
    <>
      <div className={"side-panel-overlay" + (open ? " show" : "")} onClick={onClose} />
      <div className={"side-panel" + (className ? " " + className : "") + (open ? " show" : "")}>
        <div className="side-panel-h">
          <div>
            <div style={{fontSize: 13, fontWeight: 600, letterSpacing: "-0.01em"}}>{title}</div>
            {subtitle && <div style={{fontSize: 12, color: "var(--ink-500)"}}>{subtitle}</div>}
          </div>
          <button className="btn ghost" onClick={onClose} style={{padding: "4px 8px"}}>
            <Glyph name="close" size={14} />
          </button>
        </div>
        <div className="side-panel-body">{children}</div>
      </div>
    </>
  );
}

/* ---------- Key/Value grid ---------- */
function KVGrid({ items }) {
  return (
    <div className="kv-grid">
      {items.map((it, i) => (
        <React.Fragment key={i}>
          <div className="k">{it.k}</div>
          <div className="v">{it.v ?? <span className="muted">—</span>}</div>
        </React.Fragment>
      ))}
    </div>
  );
}

/* ---------- Status cell for tables ---------- */
function StatusCell({ value, kind }) {
  if (!value) return <span className="muted" style={{fontSize:12}}>—</span>;
  const b = kind ? PRData.statusBucket(value, kind) : "muted";
  return <span className={"status-cell " + b}><span className="dot"/>{value}</span>;
}

/* ---------- Sortable data table ---------- */
function DataTable({ columns, rows, onRowClick, emptyText = "No matching rows.", maxHeight }) {
  const [sortKey, setSortKey] = useState(null);
  const [sortDir, setSortDir] = useState("asc");
  const sorted = useMemo(() => {
    if (!sortKey) return rows;
    const col = columns.find((c) => c.key === sortKey);
    if (!col) return rows;
    const cp = [...rows].sort((a, b) => {
      const av = col.sortVal ? col.sortVal(a) : a[col.key];
      const bv = col.sortVal ? col.sortVal(b) : b[col.key];
      if (av == null && bv == null) return 0;
      if (av == null) return 1;
      if (bv == null) return -1;
      if (av instanceof Date && bv instanceof Date) return av - bv;
      if (typeof av === "number" && typeof bv === "number") return av - bv;
      return String(av).localeCompare(String(bv));
    });
    if (sortDir === "desc") cp.reverse();
    return cp;
  }, [rows, sortKey, sortDir, columns]);

  if (!rows.length) return <Empty title={emptyText} />;
  return (
    <div className="table-scroll" style={maxHeight ? { maxHeight } : undefined}>
      <table className="data">
        <thead>
          <tr>
            {columns.map((c) => (
              <th key={c.key} style={{cursor: c.sortable === false ? "default" : "pointer", width: c.width}} onClick={() => {
                if (c.sortable === false) return;
                if (sortKey === c.key) setSortDir(sortDir === "asc" ? "desc" : "asc");
                else { setSortKey(c.key); setSortDir("asc"); }
              }}>
                <span style={{display:"inline-flex", alignItems:"center", gap: 4}}>
                  {c.label}
                  {sortKey === c.key && (sortDir === "asc" ? <Glyph name="arrowUp" size={10}/> : <Glyph name="arrowDown" size={10}/>)}
                </span>
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {sorted.map((r, i) => (
            <tr key={r._id || i} onClick={() => onRowClick && onRowClick(r)}>
              {columns.map((c) => (
                <td key={c.key} className={c.cellClass} style={c.cellStyle}>
                  {c.render ? c.render(r) : (r[c.key] ?? "—")}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

/* =============================================================
   PROP-02 — LifecyclePhasePill — reused across tracker pages,
                                  PM Editor, SiteDetail, monitoring
   ============================================================= */
const LIFECYCLE_PILL_TONE = {
  "no-data": "muted",
  "underlay-not-started": "muted",
  "underlay-ordering": "info",
  "underlay-delivery": "info",
  "underlay-activated": "warn",
  "overlay-not-ready": "warn",
  "overlay-ready": "warn",
  "overlay-scheduled": "warn",
  "overlay-migrated": "ok",
  "isp-termination": "ok",
  "complete": "ok",
  "blocked": "risk",
};
const LIFECYCLE_PILL_LABEL = {
  "no-data": "No data",
  "underlay-not-started": "Underlay · not started",
  "underlay-ordering": "Underlay · ordering",
  "underlay-delivery": "Underlay · delivery",
  "underlay-activated": "Underlay · activated",
  "overlay-not-ready": "Overlay · not ready",
  "overlay-ready": "Overlay · ready",
  "overlay-scheduled": "Overlay · scheduled",
  "overlay-migrated": "Overlay · migrated",
  "isp-termination": "ISP termination",
  "complete": "End-to-end complete",
  "blocked": "Blocked",
};
function LifecyclePhasePill({ phase, percent, compact }) {
  if (!phase) return null;
  const tone = LIFECYCLE_PILL_TONE[phase] || "muted";
  const label = LIFECYCLE_PILL_LABEL[phase] || phase;
  return (
    <span className={"pill " + tone} title={typeof percent === "number" ? `${label} · ${percent}%` : label}>
      <span className="dot" />
      <span>{compact ? label.replace(/^.*?·\s*/, "") : label}</span>
      {typeof percent === "number" && !compact && <span className="mono" style={{ marginLeft: 4, fontSize: 10.5, opacity: .8 }}>{percent}%</span>}
    </span>
  );
}

/* =============================================================
   PROP-03..06 — TrackerRowEditor — compact inline edit popover.
   Reused by Underlay, Overlay, ISP termination, Risk & blockers.

   props:
     row        — the model row
     entity     — 'underlay' | 'overlay' | 'isp' | 'newUnderlay'
     idResolver — (row) => { idRef, confidence, reason }
     fields     — [{ key, label, type: 'enum'|'date'|'longtext'|'text'|'boolean', options? }]
     onClose    — () => void
     onSaved    — () => void  (after successful patch)
     canEdit    — boolean
   ============================================================= */
// GA-13: format a model value for an <input> and parse it back to the stored
// patch value. Mirrors page-pm.jsx::formatForInput/parseFromInput so the tracker
// editors coerce dates/numbers identically to the PM editor.
function trackerFormatForInput(field, value) {
  if (value === null || value === undefined) return "";
  if (field.type === "date") {
    if (value instanceof Date && !isNaN(value)) return value.toISOString().slice(0, 10);
    if (typeof value === "string" && value.match(/^\d{4}-\d{2}-\d{2}/)) return value.slice(0, 10);
    return "";
  }
  if (field.type === "number") {
    if (typeof value === "number") return String(value);
    const n = parseFloat(value);
    return isNaN(n) ? "" : String(n);
  }
  return String(value);
}
function trackerParseFromInput(field, raw) {
  if (raw === "" || raw === null || raw === undefined) return "";
  if (field.type === "date") { const d = new Date(raw); return isNaN(d) ? "" : d; }
  if (field.type === "number") { const n = parseFloat(raw); return isNaN(n) ? "" : n; }
  return raw;
}

// GA-13: one autosaving field inside TrackerRowEditor. Matches the PM editor's
// FieldEditor — enum/boolean commit on change, text/date/number/longtext on blur
// (Enter blurs) — and shows a per-field saving spinner + "Saved" flash. No-ops when
// the committed value equals the current model value so reopening never re-saves.
function TrackerField({ field, value, saving, flash, disabled, onSave }) {
  const [local, setLocal] = useState(() => trackerFormatForInput(field, value));
  useEffect(() => { setLocal(trackerFormatForInput(field, value)); }, [field.key, value]);

  const commit = (next) => {
    if (next === undefined) next = local;
    const cleaned = trackerParseFromInput(field, next);
    if (trackerFormatForInput(field, cleaned) === trackerFormatForInput(field, value)) return;
    onSave(cleaned);
  };

  const input = (() => {
    if (field.type === "enum") {
      return (
        <select className="pm-input" aria-label={field.label} value={local || ""} disabled={disabled}
          onChange={(e) => { setLocal(e.target.value); onSave(trackerParseFromInput(field, e.target.value)); }}>
          <option value="">—</option>
          {(field.options || []).map((o) => <option key={o} value={o}>{o}</option>)}
        </select>
      );
    }
    if (field.type === "date") {
      return <input type="date" className="pm-input" aria-label={field.label} value={local || ""} disabled={disabled}
        onChange={(e) => setLocal(e.target.value)} onBlur={() => commit()}
        onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); }}/>;
    }
    if (field.type === "number") {
      return <input type="number" className="pm-input" aria-label={field.label} value={local ?? ""} disabled={disabled}
        onChange={(e) => setLocal(e.target.value)} onBlur={() => commit()}
        onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); }}/>;
    }
    if (field.type === "longtext") {
      return <textarea className="pm-input pm-textarea" aria-label={field.label} rows={3} value={local || ""} disabled={disabled}
        onChange={(e) => setLocal(e.target.value)} onBlur={() => commit()}/>;
    }
    if (field.type === "boolean") {
      const k = String(local || "").toLowerCase();
      return (
        <div className="pm-toggle" role="group" aria-label={field.label}>
          <button type="button" aria-label={field.label + ": Yes"} aria-pressed={k === "yes"} className={k === "yes" ? "active" : ""} disabled={disabled} onClick={() => { setLocal("Yes"); onSave("Yes"); }}>Yes</button>
          <button type="button" aria-label={field.label + ": No"} aria-pressed={k === "no"} className={k === "no" ? "active" : ""} disabled={disabled} onClick={() => { setLocal("No"); onSave("No"); }}>No</button>
          <button type="button" aria-label={field.label + ": clear"} aria-pressed={!k} className={!k ? "active" : ""} disabled={disabled} onClick={() => { setLocal(""); onSave(""); }}>—</button>
        </div>
      );
    }
    return <input type="text" className="pm-input" aria-label={field.label} value={local || ""} disabled={disabled}
      onChange={(e) => setLocal(e.target.value)} onBlur={() => commit()}
      onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); }}/>;
  })();

  return (
    <div className="pm-field" data-field-key={field.key}>
      <label className="pm-field-label">{field.label}</label>
      <div className="pm-field-input">
        {input}
        {flash && <span className="pm-saved-pill">Saved</span>}
        {saving && <span className="pm-saving-spinner"/>}
      </div>
    </div>
  );
}

function TrackerRowEditor({ row, entity, idResolver, fields, onClose, onSaved, canEdit, openInPM }) {
  const idInfo = useMemo(() => idResolver(row), [row, idResolver]);
  // GA-13: per-field autosave (enum/boolean on change, text/date/number on blur) —
  // no explicit Save click, matching the PM editor. Editing is enabled only when the
  // row resolves to a single unambiguous idRef; otherwise inputs are disabled and the
  // user is routed to "Open in PM" (the prior Save-time identity guard, enforced up
  // front so an autosave can never target the wrong row).
  const idOk = !!(idInfo && idInfo.idRef) && idInfo.confidence !== "ambiguous";
  const editable = canEdit && idOk;
  const [savingField, setSavingField] = useState(null);
  const [savedFlash, setSavedFlash] = useState(null);
  const [err, setErr] = useState("");

  const saveField = useCallback(async (field, value) => {
    if (!editable) return;
    setErr("");
    setSavingField(field.key);
    try {
      // Preserve per-field provenance: carry the workbook baseline + source so the
      // PMStore override records workbookValue/sourceEntity/sourceField (matches the
      // PM editor's handleSave). The store keeps a prior workbookValue if present.
      const priorMeta = (row && row._overrideMeta && row._overrideMeta[field.key]) || {};
      const hasPriorWb = Object.prototype.hasOwnProperty.call(priorMeta, "workbookValue") &&
        priorMeta.workbookValue !== undefined && priorMeta.workbookValue !== null;
      const fieldMeta = { sourceEntity: entity, sourceField: field.key };
      // Capture the workbook baseline only when one is known: a prior override's
      // recorded value, else the row's current value for flat tracker fields. Risk
      // readiness rows key on `readiness.<k>` (not a row property), so we omit it and
      // let PMStore preserve any prior workbookValue — never clobber it with undefined.
      if (hasPriorWb) fieldMeta.workbookValue = priorMeta.workbookValue;
      else if (row[field.key] !== undefined) fieldMeta.workbookValue = row[field.key];
      const patch = { [field.key]: value, __meta: { [field.key]: fieldMeta } };
      if (entity === "underlay") await PMClient.patchLink(idInfo.idRef, patch);
      else if (entity === "overlay") await PMClient.patchOverlay(idInfo.idRef, patch);
      else if (entity === "isp") await PMClient.patchIsp(idInfo.idRef, patch);
      else if (entity === "newUnderlay") await PMClient.patchNewUnderlay(idInfo.idRef, patch);
      else throw new Error("Unknown entity: " + entity);
      setSavedFlash(field.key);
      setTimeout(() => setSavedFlash((cur) => (cur === field.key ? null : cur)), 1500);
      onSaved && onSaved();
    } catch (e) {
      setErr("Save failed (" + field.label + "): " + (e.message || e));
    } finally {
      setSavingField((cur) => (cur === field.key ? null : cur));
    }
  }, [editable, entity, idInfo, row, onSaved]);

  return (
    <div className="pm-modal-backdrop" onClick={onClose}>
      <div className="pm-modal large" onClick={(e) => e.stopPropagation()}>
        <div className="pm-modal-h">
          <div className="title">
            Edit {entity === "underlay" ? "underlay link"
                : entity === "overlay" ? "overlay site"
                : entity === "isp" ? "ISP termination"
                : "new underlay row"}
            <span className="mono" style={{ marginLeft: 8, color: "var(--ink-500)" }}>· {idInfo && idInfo.idRef ? idInfo.idRef : "—"}</span>
            {idInfo && idInfo.confidence !== "exact" && (
              <span className={"pill " + (idInfo.confidence === "ambiguous" ? "risk" : "warn")} style={{ marginLeft: 8 }}>
                {idInfo.confidence}
              </span>
            )}
          </div>
          <button className="btn ghost" onClick={onClose}><Glyph name="close" size={14}/></button>
        </div>
        <div className="pm-modal-body">
          {/* GA-18: shared local-only vs server-backed indicator, identical to the PM
              editor — users always know where a tracker edit is stored. */}
          <StorageModePill/>
          {!canEdit && <div className="pm-readonly-pill" style={{ marginBottom: 8 }}>Read-only · management cannot edit</div>}
          {canEdit && !idOk && (
            <div className="pm-readonly-pill" style={{ marginBottom: 8, background: "var(--risk-soft)", color: "var(--risk)" }}>
              {(idInfo && idInfo.reason) || "This row cannot be identified safely"}. Use "Open in PM" to pick the right row.
            </div>
          )}
          {canEdit && idOk && (
            <div className="pm-readonly-pill" style={{ marginBottom: 8 }}>Changes autosave as you edit — no Save button.</div>
          )}
          <div className="grid g-2" style={{ gap: 10 }}>
            {fields.map((f) => (
              <TrackerField
                key={f.key}
                field={f}
                value={row[f.key]}
                saving={savingField === f.key}
                flash={savedFlash === f.key}
                disabled={!editable}
                onSave={(v) => saveField(f, v)}
              />
            ))}
          </div>
          {err && <div className="pm-readonly-pill" style={{ marginTop: 10, background: "var(--risk-soft)", color: "var(--risk)" }}>{err}</div>}
        </div>
        <div className="pm-modal-actions">
          {openInPM && idInfo && idInfo.idRef && (
            <button className="btn" onClick={() => { onClose(); openInPM(entity, idInfo.idRef); }}>
              <Glyph name="ext" size={12}/> Open in PM
            </button>
          )}
          <span style={{ flex: 1 }}/>
          <button className="btn primary" onClick={onClose}>Done</button>
        </div>
      </div>
    </div>
  );
}

/* ---------- Storage-mode helper + shared pill (GA-18; was PMNU-11/30 in page-pm) ----------
   Single source of the "where do my edits go" wording AND the one component every
   edit surface renders, so neither wording nor markup can fork across surfaces.
   PMClient.kind is "azure" only when a backend base URL was explicitly configured at
   boot (window.AZURE_API_BASE / ONENET_SERVER_CONFIG.apiBaseUrl); the standalone
   LocalClient default ("local") stores edits in this browser's PMStore. This lives in
   components.jsx (loaded first) so PM, tracker editors, readiness editors and the
   discussion rail all share it. Read-only — it never forces backend mode. */
function pmStorageMode() {
  const clientKind = (typeof PMClient !== "undefined" && PMClient && PMClient.kind) || "local";
  const serverBacked = clientKind === "azure";
  return {
    serverBacked,
    kind: clientKind,
    mode: serverBacked ? "server" : "local",
    label: serverBacked ? "shared · server-backed" : "local-only",
    note: serverBacked
      ? "Edits save to the shared backend and are visible to other users."
      : "Edits are saved in this browser only (PMStore) until you export the workbook or snapshot.",
    title: serverBacked
      ? "Server-backed: edits save to the shared backend and are visible to other users."
      : "Local-only: edits are stored in this browser (PMStore) and are not shared with other users until you export the workbook or snapshot.",
  };
}

// One pill, every edit surface. `compact` renders just the bare pill (tight
// headers / discussion / section bars); the default renders the editor-wide bar
// with a one-line operational note. Both carry data-pm-storage-mode (bar) and
// data-nu-mode (pill) so cross-surface assertions resolve consistently.
function StorageModePill({ compact = false, note = true, className }) {
  const storage = pmStorageMode();
  const pill = (
    <span
      className={"pm-nu-mode " + (storage.serverBacked ? "pm-nu-mode-server" : "pm-nu-mode-local")}
      data-nu-mode={storage.mode}
      data-pm-storage-mode={storage.mode}
      title={storage.title}
    >
      {storage.label}
    </span>
  );
  if (compact) return pill;
  return (
    <div className={"pm-edit-modebar" + (className ? " " + className : "")} data-pm-storage-mode={storage.mode}>
      {pill}
      {note && <span className="pm-edit-mode-note">{storage.note}</span>}
    </div>
  );
}

/* ---------- export ---------- */
Object.assign(window, {
  Glyph, KPI, Pill, CriticalityPill, StatusPill, RiskPill,
  Progress, Empty, Skeleton, FilterSelect, SearchInput, Toast,
  PageHeader, Tabs, SidePanel, KVGrid, StatusCell, DataTable,
  LifecyclePhasePill, TrackerRowEditor,
  pmStorageMode, StorageModePill,
});
