/* Verndale Commerce — Meeting Governance interactive map */
const { useState, useRef, useLayoutEffect, useEffect, useCallback, useMemo } = React;

const BASE_MEETINGS = window.MEETINGS;
const BASE_EDGES = window.EDGES;
const FLOWS = window.FLOWS;
const TIERS = window.TIERS;
const FLOW_BY_ID = Object.fromEntries(FLOWS.map(f => [f.id, f]));
const TIER_ORDER = { leadership: 0, practice: 1, project: 2 };
const edgeKey = e => `${e.from}__${e.to}__${e.flow}`;

/* ---------- persistence (Cloudflare KV via Pages Function) ---------- */
const IS_LOCAL = window.location.hostname === "localhost";
const API = IS_LOCAL
  ? "http://localhost:3501/api/meeting-governance/state"
  : "/api/meeting-governance/state";
const LOCAL_SNAPSHOT = "/meeting-governance/.local-kv.json";
function apiGet() {
  return fetch(API).then(r => {
    if (!r.ok) throw new Error(r.status);
    return r.json();
  }).catch(() =>
    fetch(LOCAL_SNAPSHOT).then(r => r.ok ? r.json() : { edits: { meetings: {}, edges: {} }, notes: "" })
  ).catch(() => ({ edits: { meetings: {}, edges: {} }, notes: "" }));
}
function apiPut(key, value) {
  return fetch(API, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key, value }) });
}

/* ---------- Outlook (.ics) meeting template ---------- */
function pad(n) { return String(n).padStart(2, "0"); }
function rruleFor(freq) {
  const f = (freq || "").toLowerCase();
  if (f.includes("bi-weekly") || f.includes("biweekly")) return "FREQ=WEEKLY;INTERVAL=2";
  if (f.includes("week")) return "FREQ=WEEKLY";
  if (f.includes("quarter")) return "FREQ=MONTHLY;INTERVAL=3";
  if (f.includes("month")) return "FREQ=MONTHLY";
  return null;
}
function durISO(d) {
  const h = /(\d+)\s*h/.exec(d || ""); const m = /(\d+)\s*min/.exec(d || "");
  if (h) return "PT" + h[1] + "H";
  if (m) return "PT" + m[1] + "M";
  return "PT30M";
}
function icsEscape(s) {
  return String(s).replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\r?\n/g, "\\n");
}
function fold(line) {
  const out = [];
  while (line.length > 73) { out.push(line.slice(0, 73)); line = " " + line.slice(73); }
  out.push(line);
  return out.join("\r\n");
}
function meetingDescription(m) {
  const L = [];
  L.push("FOCUS: " + m.focus, "");
  L.push("DRIVER: " + FLOW_BY_ID[m.driver].label);
  L.push("OWNER: " + m.owner);
  L.push("PARTICIPANTS: " + m.participants.join(", "), "");
  L.push("AGENDA:");
  m.agenda.forEach(ph => {
    L.push("• " + ph.phase + "  (" + ph.time + ")");
    ph.items.forEach(it => L.push("    – " + it));
  });
  L.push("", "Status today: " + (m.inPlace === "yes" ? "Existing meeting" : "Proposed meeting"));
  L.push("Template generated from the Verndale Commerce Meeting Governance map.");
  return L.join("\n");
}
function downloadInvite(m) {
  const now = new Date();
  const start = new Date(now);
  const day = start.getDay();
  const add = ((8 - day) % 7) || 7;
  start.setDate(start.getDate() + add);
  start.setHours(10, 0, 0, 0);
  const fmtLocal = dt => "" + dt.getFullYear() + pad(dt.getMonth() + 1) + pad(dt.getDate()) + "T" + pad(dt.getHours()) + pad(dt.getMinutes()) + "00";
  const fmtUTC = dt => "" + dt.getUTCFullYear() + pad(dt.getUTCMonth() + 1) + pad(dt.getUTCDate()) + "T" + pad(dt.getUTCHours()) + pad(dt.getUTCMinutes()) + pad(dt.getUTCSeconds()) + "Z";
  const rr = rruleFor(m.frequency);
  const lines = [
    "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Verndale//Commerce Meeting Governance//EN",
    "CALSCALE:GREGORIAN", "METHOD:PUBLISH", "BEGIN:VEVENT",
    "UID:" + m.id + "-" + Date.now() + "@verndale.com",
    "DTSTAMP:" + fmtUTC(now), "DTSTART:" + fmtLocal(start), "DURATION:" + durISO(m.duration)
  ];
  if (rr) lines.push("RRULE:" + rr);
  lines.push(
    "SUMMARY:" + icsEscape("Commerce — " + m.name),
    "DESCRIPTION:" + icsEscape(meetingDescription(m)),
    "ORGANIZER;CN=" + icsEscape(m.owner) + ":mailto:noreply@verndale.com",
    "TRANSP:OPAQUE", "END:VEVENT", "END:VCALENDAR"
  );
  const ics = lines.map(fold).join("\r\n");
  const blob = new Blob([ics], { type: "text/calendar;charset=utf-8" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = m.name.replace(/[^\w]+/g, "_").replace(/^_|_$/g, "") + ".ics";
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1500);
}

/* ---------- icons ---------- */
function CalIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
      <rect x="1.5" y="2.5" width="13" height="12" stroke="currentColor" strokeWidth="1.4" />
      <path d="M1.5 5.5H14.5" stroke="currentColor" strokeWidth="1.4" />
      <path d="M5 1V4M11 1V4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
    </svg>
  );
}
function PencilIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
      <path d="M11 2.5L13.5 5L5 13.5L2 14L2.5 11L11 2.5Z" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round" />
    </svg>
  );
}
function NoteIcon() {
  return (
    <svg width="15" height="15" viewBox="0 0 16 16" fill="none" aria-hidden="true">
      <path d="M3 1.5H10L13 4.5V14.5H3V1.5Z" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round" />
      <path d="M9.5 1.5V5H13" stroke="currentColor" strokeWidth="1.4" strokeLinejoin="round" />
      <path d="M5.5 8H10.5M5.5 10.5H10.5M5.5 5.5H7" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
    </svg>
  );
}
function TrashIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
      <path d="M2 4H14M5 4V2.5C5 2 5.5 1.5 6 1.5H10C10.5 1.5 11 2 11 2.5V4M6 7V12M10 7V12M3 4L4 13.5H12L13 4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );
}
function PlusIcon() {
  return (
    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
      <path d="M8 2V14M2 8H14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/>
    </svg>
  );
}

/* ---------- Notes panel ---------- */
function NotesPanel({ open, value, onChange, onClose }) {
  const taRef = useRef(null);
  useEffect(() => { if (open && taRef.current) taRef.current.focus(); }, [open]);
  if (!open) return null;
  const count = (value || "").trim().length;
  return (
    <div className="notes-panel" role="dialog" aria-label="Notes">
      <div className="notes-head">
        <span className="notes-title"><NoteIcon /> Notes &amp; to-dos</span>
        <button className="notes-close" onClick={onClose} aria-label="Close notes">✕</button>
      </div>
      <textarea
        ref={taRef}
        className="notes-area"
        value={value}
        placeholder={"Jot down open questions, follow-ups, decisions…\n\n— Confirm Weekly Leadership cadence\n— Who owns the All Hands?\n— Validate Office Hours frequency with client"}
        onChange={e => onChange(e.target.value)}
      />
      <div className="notes-foot">
        <span>{count ? count + " characters · saved automatically" : "Saved automatically to this browser"}</span>
      </div>
    </div>
  );
}

/* ---------- orthogonal path ---------- */
function roundedPath(pts, r) {
  if (pts.length < 2) return "";
  let d = `M ${pts[0].x} ${pts[0].y}`;
  for (let i = 1; i < pts.length - 1; i++) {
    const p0 = pts[i - 1], p1 = pts[i], p2 = pts[i + 1];
    const v1 = { x: p1.x - p0.x, y: p1.y - p0.y };
    const v2 = { x: p2.x - p1.x, y: p2.y - p1.y };
    const l1 = Math.hypot(v1.x, v1.y) || 1, l2 = Math.hypot(v2.x, v2.y) || 1;
    const rr = Math.min(r, l1 / 2, l2 / 2);
    const a = { x: p1.x - (v1.x / l1) * rr, y: p1.y - (v1.y / l1) * rr };
    const b = { x: p1.x + (v2.x / l2) * rr, y: p1.y + (v2.y / l2) * rr };
    d += ` L ${a.x.toFixed(1)} ${a.y.toFixed(1)} Q ${p1.x.toFixed(1)} ${p1.y.toFixed(1)} ${b.x.toFixed(1)} ${b.y.toFixed(1)}`;
  }
  const last = pts[pts.length - 1];
  d += ` L ${last.x.toFixed(1)} ${last.y.toFixed(1)}`;
  return d;
}

/* ---------- small editable inputs ---------- */
function TextField({ value, onChange, placeholder, className }) {
  return <input className={"edit-input " + (className || "")} value={value || ""} placeholder={placeholder}
    onChange={e => onChange(e.target.value)} />;
}
function AreaField({ value, onChange, placeholder, className }) {
  return <textarea className={"edit-input " + (className || "")} value={value || ""} placeholder={placeholder}
    rows={2} onChange={e => onChange(e.target.value)} />;
}

/* ---------- Card ---------- */
function MeetingCard({ m, state, onHover, onLeave, onClick, refCb }) {
  const proposed = m.inPlace !== "yes";
  const cls = ["card", `lane-${m.visibility}`, proposed ? "is-proposed" : "is-existing", `st-${state}`].join(" ");
  return (
    <div ref={refCb} className={cls}
      onMouseEnter={() => onHover(m.id)} onMouseLeave={onLeave} onClick={() => onClick(m.id)}>
      <div className="card-bar" />
      <div className="card-headrow">
        <span className="driver-badge" style={{ background: FLOW_BY_ID[m.driver].color }}>{FLOW_BY_ID[m.driver].tag}</span>
        {proposed ? <span className="tag-proposed">Proposed</span> : null}
      </div>
      <div className="card-top">
        <div className="card-titles">
          <div className="card-title">{m.name}</div>
          {m.sub ? <div className="card-sub">{m.sub}</div> : null}
        </div>
      </div>
      <div className="card-owner">
        <span className="owner-label">Owner</span>
        <span className="owner-name">{m.owner}</span>
      </div>
      <div className="card-meta">
        <span className="meta-chip">{m.frequency}</span>
        <span className="meta-dot">·</span>
        <span className="meta-chip">{m.duration}</span>
      </div>
      <div className="card-peek">{m.focus}</div>
    </div>
  );
}

/* ---------- Add Meeting Modal ---------- */
function AddMeetingModal({ onSave, onClose }) {
  const [form, setForm] = useState({
    name: "", sub: "", tier: "project", visibility: "internal",
    driver: "pm", frequency: "Weekly", duration: "60 min",
    owner: "", focus: "", inPlace: "no", category: ""
  });
  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));

  const save = () => {
    if (!form.name.trim()) return;
    const id = "custom_" + Date.now();
    onSave({
      id, name: form.name.trim(), sub: form.sub, tier: form.tier,
      visibility: form.visibility, driver: form.driver,
      frequency: form.frequency, duration: form.duration,
      owner: form.owner, focus: form.focus, inPlace: form.inPlace,
      category: form.category, participants: [], agenda: [], rollsUpTo: []
    });
    onClose();
  };

  return (
    <React.Fragment>
      <div className="drawer-scrim" onClick={onClose} />
      <aside className="drawer">
        <div className="drawer-head">
          <div className="drawer-eyebrow">
            <span style={{fontWeight:700,fontSize:13,letterSpacing:".04em",textTransform:"uppercase"}}>New Meeting</span>
          </div>
          <div className="drawer-head-btns">
            <button className={"drawer-edit" + (form.name.trim() ? " active" : "")} onClick={save} disabled={!form.name.trim()}>
              Save
            </button>
            <button className="drawer-close" onClick={onClose} aria-label="Cancel">✕</button>
          </div>
        </div>
        <div className="edit-meta">
          <label>Status
            <select value={form.inPlace} onChange={e => set("inPlace", e.target.value)}>
              <option value="yes">In place today</option>
              <option value="no">Proposed</option>
              <option value="tbd">TBD</option>
            </select>
          </label>
          <label>Driver
            <select value={form.driver} onChange={e => set("driver", e.target.value)}>
              {FLOWS.map(f => <option key={f.id} value={f.id}>{f.label}</option>)}
            </select>
          </label>
          <label>Tier
            <select value={form.tier} onChange={e => set("tier", e.target.value)}>
              {TIERS.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}
            </select>
          </label>
          <label>Audience
            <select value={form.visibility} onChange={e => set("visibility", e.target.value)}>
              <option value="internal">Internal</option>
              <option value="customer">Customer</option>
            </select>
          </label>
        </div>
        <TextField className="edit-title" value={form.name} onChange={v => set("name", v)} placeholder="Meeting name *" />
        <TextField className="edit-sub" value={form.sub} onChange={v => set("sub", v)} placeholder="Subtitle (optional)" />
        <div className="drawer-facts">
          {[["category","Category"],["frequency","Frequency"],["duration","Duration"],["owner","Owner"]].map(([k, label]) => (
            <div className="fact" key={k}>
              <span className="fact-k">{label}</span>
              <TextField value={form[k]} onChange={v => set(k, v)} />
            </div>
          ))}
        </div>
        <AreaField className="edit-focus" value={form.focus} onChange={v => set("focus", v)} placeholder="Focus / purpose" />
      </aside>
    </React.Fragment>
  );
}

/* ---------- Detail Drawer ---------- */
function Drawer({ m, byId, edges, editing, setEditing, onClose, onJump, onDelete, updateMeeting, updateEdgeLabel }) {
  if (!m) return null;
  const proposed = m.inPlace !== "yes";
  const set = (key, val) => updateMeeting(m.id, { [key]: val });

  const feedsEdges = edges.filter(e => e.from === m.id && byId[e.to]);
  const fedEdges = edges.filter(e => e.to === m.id && byId[e.from]);

  // participant helpers
  const setPart = (i, v) => { const a = [...m.participants]; a[i] = v; set("participants", a); };
  const addPart = () => set("participants", [...m.participants, "New participant"]);
  const delPart = i => set("participants", m.participants.filter((_, j) => j !== i));

  // agenda helpers
  const setPhase = (i, key, v) => { const a = m.agenda.map((p, j) => j === i ? { ...p, [key]: v } : p); set("agenda", a); };
  const addPhase = () => set("agenda", [...m.agenda, { phase: "New section", time: "", items: ["New item"] }]);
  const delPhase = i => set("agenda", m.agenda.filter((_, j) => j !== i));
  const setItem = (pi, ii, v) => { const a = m.agenda.map((p, j) => j === pi ? { ...p, items: p.items.map((it, k) => k === ii ? v : it) } : p); set("agenda", a); };
  const addItem = pi => { const a = m.agenda.map((p, j) => j === pi ? { ...p, items: [...p.items, "New item"] } : p); set("agenda", a); };
  const delItem = (pi, ii) => { const a = m.agenda.map((p, j) => j === pi ? { ...p, items: p.items.filter((_, k) => k !== ii) } : p); set("agenda", a); };

  return (
    <React.Fragment>
      <div className="drawer-scrim" onClick={onClose} />
      <aside className={`drawer lane-${m.visibility} ${editing ? "is-editing" : ""}`}>
        <div className="drawer-head">
          <div className="drawer-eyebrow">
            <span className={`lane-pill lane-${m.visibility}`}>{m.visibility === "internal" ? "Internal" : "Customer"}</span>
            <span className="tier-pill">{TIERS.find(t => t.id === m.tier).label}</span>
            <span className="driver-pill" style={{ background: FLOW_BY_ID[m.driver].color }}>{FLOW_BY_ID[m.driver].short}</span>
            {proposed ? <span className="tag-proposed solid">Proposed</span> : <span className="tag-existing">In place today</span>}
          </div>
          <div className="drawer-head-btns">
            <button className={"drawer-edit " + (editing ? "active" : "")} onClick={() => setEditing(!editing)} title="Toggle edit mode">
              <PencilIcon />{editing ? "Done" : "Edit"}
            </button>
            <button className="drawer-delete" onClick={() => onDelete(m.id, m.name)} title="Delete meeting">
              <TrashIcon />Delete
            </button>
            <button className="drawer-close" onClick={onClose} aria-label="Close">✕</button>
          </div>
        </div>

        {editing ? (
          <div className="edit-meta">
            <label>Status
              <select value={m.inPlace} onChange={e => set("inPlace", e.target.value)}>
                <option value="yes">In place today</option>
                <option value="no">Proposed</option>
                <option value="tbd">TBD</option>
              </select>
            </label>
            <label>Driver
              <select value={m.driver} onChange={e => set("driver", e.target.value)}>
                {FLOWS.map(f => <option key={f.id} value={f.id}>{f.label}</option>)}
              </select>
            </label>
            <label>Tier
              <select value={m.tier} onChange={e => set("tier", e.target.value)}>
                {TIERS.map(t => <option key={t.id} value={t.id}>{t.label}</option>)}
              </select>
            </label>
            <label>Audience
              <select value={m.visibility} onChange={e => set("visibility", e.target.value)}>
                <option value="internal">Internal</option>
                <option value="customer">Customer</option>
              </select>
            </label>
          </div>
        ) : null}

        {editing
          ? <TextField className="edit-title" value={m.name} onChange={v => set("name", v)} placeholder="Meeting name" />
          : <h2 className="drawer-title">{m.name}</h2>}
        {editing
          ? <TextField className="edit-sub" value={m.sub} onChange={v => set("sub", v)} placeholder="Subtitle (optional)" />
          : (m.sub ? <div className="drawer-sub">{m.sub}</div> : null)}

        <div className="drawer-facts">
          {[["category", "Category"], ["frequency", "Frequency"], ["duration", "Duration"], ["owner", "Owner"]].map(([k, label]) => (
            <div className="fact" key={k}>
              <span className="fact-k">{label}</span>
              {editing ? <TextField value={m[k]} onChange={v => set(k, v)} /> : <span className="fact-v">{m[k]}</span>}
            </div>
          ))}
        </div>

        {editing
          ? <AreaField className="edit-focus" value={m.focus} onChange={v => set("focus", v)} placeholder="Focus / purpose" />
          : <div className="drawer-focus">{m.focus}</div>}

        <div className="drawer-actions">
          <button className="btn-outlook" onClick={() => downloadInvite(m)}>
            <CalIcon /><span>Create Outlook invite</span>
          </button>
          <span className="action-hint">Downloads an .ics template with the agenda, owner &amp; cadence</span>
        </div>

        <div className="drawer-sec">
          <div className="sec-label">Participants</div>
          <div className={"chips " + (editing ? "editing" : "")}>
            {m.participants.map((p, i) => editing ? (
              <span key={i} className="chip edit-chip">
                <TextField value={p} onChange={v => setPart(i, v)} />
                <button className="mini-del" onClick={() => delPart(i)} title="Remove">✕</button>
              </span>
            ) : <span key={i} className="chip">{p}</span>)}
            {editing ? <button className="mini-add" onClick={addPart}>+ Add</button> : null}
          </div>
        </div>

        <div className="drawer-sec">
          <div className="sec-label">Agenda</div>
          <ol className="agenda">
            {m.agenda.map((ph, i) => (
              <li key={i} className="agenda-phase">
                <div className="phase-head">
                  {editing ? (
                    <React.Fragment>
                      <TextField className="edit-phase" value={ph.phase} onChange={v => setPhase(i, "phase", v)} placeholder="Section" />
                      <TextField className="edit-time" value={ph.time} onChange={v => setPhase(i, "time", v)} placeholder="Owner — time" />
                      <button className="mini-del" onClick={() => delPhase(i)} title="Remove section">✕</button>
                    </React.Fragment>
                  ) : (
                    <React.Fragment>
                      <span className="phase-name">{ph.phase}</span>
                      <span className="phase-time">{ph.time}</span>
                    </React.Fragment>
                  )}
                </div>
                <ul className="phase-items">
                  {ph.items.map((it, j) => (
                    <li key={j}>
                      {editing ? (
                        <span className="edit-item-row">
                          <TextField value={it} onChange={v => setItem(i, j, v)} />
                          <button className="mini-del" onClick={() => delItem(i, j)} title="Remove">✕</button>
                        </span>
                      ) : it}
                    </li>
                  ))}
                </ul>
                {editing ? <button className="mini-add" onClick={() => addItem(i)}>+ Item</button> : null}
              </li>
            ))}
          </ol>
          {editing ? <button className="mini-add block" onClick={addPhase}>+ Add section</button> : null}
        </div>

        <div className="drawer-flow-edit">
          <div className="flow-block">
            <div className="sec-label">Feeds into ↑</div>
            {feedsEdges.length ? feedsEdges.map(e => (
              <div className="flow-row" key={edgeKey(e)}>
                <button className="flow-link" onClick={() => onJump(e.to)}>
                  <span className="flow-dot" style={{ background: FLOW_BY_ID[e.flow].color }} />
                  {byId[e.to].name}
                </button>
                {editing
                  ? <TextField className="edit-flow-label" value={e.label} placeholder="What information flows here…" onChange={v => updateEdgeLabel(edgeKey(e), v)} />
                  : (e.label ? <div className="flow-label">“{e.label}”</div> : <div className="flow-label muted">No label — add one in Edit</div>)}
              </div>
            )) : <div className="flow-none">Apex — top of the chain</div>}
          </div>
          <div className="flow-block">
            <div className="sec-label">Fed by ↓</div>
            {fedEdges.length ? fedEdges.map(e => (
              <div className="flow-row" key={edgeKey(e)}>
                <button className="flow-link" onClick={() => onJump(e.from)}>
                  <span className="flow-dot" style={{ background: FLOW_BY_ID[e.flow].color }} />
                  {byId[e.from].name}
                </button>
                {editing
                  ? <TextField className="edit-flow-label" value={e.label} placeholder="What information flows here…" onChange={v => updateEdgeLabel(edgeKey(e), v)} />
                  : (e.label ? <div className="flow-label">“{e.label}”</div> : <div className="flow-label muted">No label — add one in Edit</div>)}
              </div>
            )) : <div className="flow-none">Source meeting</div>}
          </div>
        </div>
      </aside>
    </React.Fragment>
  );
}

/* ---------- Canvas with cards + SVG edges ---------- */
function Canvas({ meetings, edges, byId, trace, flowVisible, hoverId, selectedId, setHoverId, setSelectedId }) {
  const wrapRef = useRef(null);
  const cardRefs = useRef({});
  const [paths, setPaths] = useState([]);
  const [dims, setDims] = useState({ w: 0, h: 0 });

  const activeSet = (hoverId || selectedId) ? trace(hoverId || selectedId) : null;
  const focusId = hoverId || selectedId;

  const compute = useCallback(() => {
    const wrap = wrapRef.current;
    if (!wrap) return;
    const base = wrap.getBoundingClientRect();
    const rects = {};
    Object.entries(cardRefs.current).forEach(([id, el]) => {
      if (!el) return;
      const r = el.getBoundingClientRect();
      rects[id] = { x: r.left - base.left, y: r.top - base.top, w: r.width, h: r.height };
    });
    const fromGroups = {}, toGroups = {};
    edges.forEach((e, i) => {
      (fromGroups[e.from] = fromGroups[e.from] || []).push(i);
      (toGroups[e.to] = toGroups[e.to] || []).push(i);
    });
    const out = edges.map((e, i) => {
      const s = rects[e.from], t = rects[e.to];
      if (!s || !t) return null;
      const fg = fromGroups[e.from], tg = toGroups[e.to];
      const fi = fg.indexOf(i), fn = fg.length;
      const ti = tg.indexOf(i), tn = tg.length;
      const sax = s.x + s.w * ((fi + 1) / (fn + 1));
      const tax = t.x + t.w * ((ti + 1) / (tn + 1));
      const tBottom = t.y + t.h;
      const sameTier = byId[e.from].tier === byId[e.to].tier;
      let pts, channelY;
      if (!sameTier) {
        const sTop = s.y;
        const mid = (sTop + tBottom) / 2;
        const chOff = (ti - (tn - 1) / 2) * 7;
        channelY = Math.min(sTop - 14, Math.max(tBottom + 14, mid + chOff));
        pts = [{ x: sax, y: sTop }, { x: sax, y: channelY }, { x: tax, y: channelY }, { x: tax, y: tBottom }];
      } else {
        const sBottom = s.y + s.h;
        const chOff = (ti - (tn - 1) / 2) * 7;
        channelY = Math.max(sBottom, tBottom) + 36 + chOff;
        pts = [{ x: sax, y: sBottom }, { x: sax, y: channelY }, { x: tax, y: channelY }, { x: tax, y: tBottom }];
      }
      const d = roundedPath(pts, 10);
      return {
        id: edgeKey(e), from: e.from, to: e.to, flow: e.flow, color: FLOW_BY_ID[e.flow].color, d,
        label: e.label || "", labelX: (sax + tax) / 2, labelY: channelY - 8
      };
    }).filter(Boolean);
    setPaths(out);
    setDims({ w: wrap.scrollWidth, h: wrap.scrollHeight });
  }, [edges, byId, meetings]);

  useLayoutEffect(() => {
    compute();
    const wrap = wrapRef.current;
    const ro = new ResizeObserver(() => compute());
    if (wrap) ro.observe(wrap);
    window.addEventListener("resize", compute);
    const t1 = setTimeout(compute, 120);
    const t2 = setTimeout(compute, 500);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(compute);
    return () => { ro.disconnect(); window.removeEventListener("resize", compute); clearTimeout(t1); clearTimeout(t2); };
  }, [compute]);

  const cardState = (id) => {
    if (!activeSet) return "idle";
    if (id === focusId) return "focus";
    if (activeSet.has(id)) return "active";
    return "dim";
  };

  const visiblePaths = paths.filter(p => flowVisible[p.flow]);
  const orderedPaths = [...visiblePaths].sort((a, b) => {
    const aa = activeSet && activeSet.has(a.from) && activeSet.has(a.to) ? 1 : 0;
    const bb = activeSet && activeSet.has(b.from) && activeSet.has(b.to) ? 1 : 0;
    return aa - bb;
  });
  // labels only on the focused meeting's own direct feed lines (in + out)
  const labelPaths = focusId
    ? visiblePaths.filter(p => p.label && (p.from === focusId || p.to === focusId))
    : [];

  return (
    <div className="canvas-scroll">
      <div className="canvas" ref={wrapRef}>
        <svg className="edges" width={dims.w} height={dims.h} style={{ width: dims.w, height: dims.h }}>
          <defs>
            <marker id="ah" markerWidth="9" markerHeight="9" refX="6.5" refY="3" orient="auto" markerUnits="userSpaceOnUse">
              <path d="M0,0 L6.5,3 L0,6 Z" fill="context-stroke" />
            </marker>
          </defs>
          {orderedPaths.map(p => {
            const isActive = activeSet && activeSet.has(p.from) && activeSet.has(p.to);
            const dim = activeSet && !isActive;
            return (
              <path key={p.id} d={p.d} style={{ stroke: p.color }}
                className={"edge " + (isActive ? "edge-active" : dim ? "edge-dim" : "edge-base")}
                markerEnd="url(#ah)" />
            );
          })}
          {labelPaths.map(p => (
            <text key={"l-" + p.id} className="edge-label" x={p.labelX} y={p.labelY}
              textAnchor="middle" style={{ fill: p.color }}>{p.label}</text>
          ))}
        </svg>

        <div className="grid">
          <div className="corner"><span className="corner-up">Information flows up ↑</span></div>
          <div className="lane-head lane-internal"><span className="lane-dot" /> Internal — Verndale</div>
          <div className="lane-head lane-customer"><span className="lane-dot" /> Customer</div>

          {TIERS.map(tier => (
            <React.Fragment key={tier.id}>
              <div className="tier-label">
                <span className="tier-level">{tier.level}</span>
                <span className="tier-name">{tier.label}</span>
                <span className="tier-note">{tier.note}</span>
              </div>
              {["internal", "customer"].map(lane => (
                <div className={`cell lane-${lane}`} key={lane}>
                  {meetings.filter(m => m.tier === tier.id && m.visibility === lane).map(m => (
                    <MeetingCard key={m.id} m={m} state={cardState(m.id)}
                      refCb={el => (cardRefs.current[m.id] = el)}
                      onHover={setHoverId} onLeave={() => setHoverId(null)} onClick={setSelectedId} />
                  ))}
                </div>
              ))}
            </React.Fragment>
          ))}
        </div>
      </div>
    </div>
  );
}

/* ---------- App ---------- */
function App() {
  const [edits, setEdits] = useState({ meetings: {}, edges: {}, deletedMeetings: [], addedMeetings: [] });
  const [hoverId, setHoverId] = useState(null);
  const [selectedId, setSelectedId] = useState(null);
  const [flowVisible, setFlowVisible] = useState({ pm: true, eng: true, sales: true });
  const [editing, setEditing] = useState(false);
  const [notesOpen, setNotesOpen] = useState(false);
  const [addingMeeting, setAddingMeeting] = useState(false);
  const [notes, setNotes] = useState("");
  const [loaded, setLoaded] = useState(false);
  const updateNotes = useCallback(v => { setNotes(v); apiPut("notes", v); }, []);

  useEffect(() => {
    apiGet().then(({ edits: e, notes: n }) => {
      setEdits({ meetings: {}, edges: {}, deletedMeetings: [], addedMeetings: [], ...e });
      setNotes(n);
    }).catch(() => {}).finally(() => setLoaded(true));
  }, []);

  const meetings = useMemo(() => {
    const deleted = new Set(edits.deletedMeetings || []);
    const base = BASE_MEETINGS
      .filter(m => !deleted.has(m.id))
      .map(m => ({ ...m, ...(edits.meetings[m.id] || {}) }));
    const added = (edits.addedMeetings || []).map(m => ({ ...m, ...(edits.meetings[m.id] || {}) }));
    return [...base, ...added];
  }, [edits]);
  const edges = useMemo(() => BASE_EDGES.map(e => ({ ...e, ...(edits.edges[edgeKey(e)] || {}) })), [edits]);
  const byId = useMemo(() => Object.fromEntries(meetings.map(m => [m.id, m])), [meetings]);

  const adj = useMemo(() => {
    const OUT = {}, IN = {};
    meetings.forEach(m => { OUT[m.id] = new Set(); IN[m.id] = new Set(); });
    edges.forEach(e => { if (OUT[e.from]) OUT[e.from].add(e.to); if (IN[e.to]) IN[e.to].add(e.from); });
    return { OUT, IN };
  }, [meetings, edges]);

  const trace = useCallback((id) => {
    const { OUT, IN } = adj;
    const down = new Set(), up = new Set();
    (function d(n) { (OUT[n] || []).forEach(x => { if (!down.has(x)) { down.add(x); d(x); } }); })(id);
    (function u(n) { (IN[n] || []).forEach(x => { if (!up.has(x)) { up.add(x); u(x); } }); })(id);
    return new Set([id, ...down, ...up]);
  }, [adj]);

  const updateMeeting = useCallback((id, patch) => {
    setEdits(prev => {
      const next = { ...prev, meetings: { ...prev.meetings, [id]: { ...(prev.meetings[id] || {}), ...patch } } };
      apiPut("edits", next); return next;
    });
  }, []);
  const updateEdgeLabel = useCallback((key, label) => {
    setEdits(prev => {
      const next = { ...prev, edges: { ...prev.edges, [key]: { ...(prev.edges[key] || {}), label } } };
      apiPut("edits", next); return next;
    });
  }, []);
  const deleteMeeting = useCallback((id, name) => {
    if (!window.confirm(`Delete "${name}"?`)) return;
    setEdits(prev => {
      const addedMeetings = prev.addedMeetings || [];
      if (addedMeetings.some(m => m.id === id)) {
        const next = { ...prev, addedMeetings: addedMeetings.filter(m => m.id !== id) };
        apiPut("edits", next);
        return next;
      }
      const next = { ...prev, deletedMeetings: [...(prev.deletedMeetings || []), id] };
      apiPut("edits", next);
      return next;
    });
    setSelectedId(null);
  }, []);

  const addMeeting = useCallback((meeting) => {
    setEdits(prev => {
      const next = { ...prev, addedMeetings: [...(prev.addedMeetings || []), meeting] };
      apiPut("edits", next);
      return next;
    });
  }, []);

  const toggleFlow = id => setFlowVisible(v => ({ ...v, [id]: !v[id] }));

  useEffect(() => {
    const onKey = e => { if (e.key === "Escape") setSelectedId(null); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  const sel = selectedId ? byId[selectedId] : null;

  if (!loaded) return <div style={{display:"flex",alignItems:"center",justifyContent:"center",height:"100vh",fontFamily:"inherit",color:"#888",fontSize:14}}>Loading…</div>;

  return (
    <div className="app">
      <header className="topbar">
        <div className="brand">
          <span className="brand-name">Verndale</span>
          <span className="brand-accent" />
        </div>
        <div className="title-block">
          <h1 className="app-title">Commerce Meeting Governance</h1>
          <p className="app-subtitle">How internal and customer meetings connect — and where information rolls up</p>
        </div>
        <button className="topbar-add" onClick={() => setAddingMeeting(true)}>
          <PlusIcon /><span>Meeting</span>
        </button>
        <button className={"topbar-notes " + (notesOpen ? "active" : "")} onClick={() => setNotesOpen(o => !o)}>
          <NoteIcon /><span>Notes</span>
          {notes.trim() ? <span className="notes-badge" /> : null}
        </button>
        <div className="legend">
          <div className="legend-group">
            <span className="lg-item"><span className="lg-sw lane-internal" />Internal</span>
            <span className="lg-item"><span className="lg-sw lane-customer" />Customer</span>
            <span className="lg-item"><span className="lg-line solid" />Existing</span>
            <span className="lg-item"><span className="lg-line dashed" />Proposed</span>
          </div>
          <div className="legend-group flow-legend">
            <span className="flow-legend-label">Information flow</span>
            {FLOWS.map(f => (
              <button key={f.id}
                className={"lg-item flow-toggle" + (flowVisible[f.id] ? "" : " is-off")}
                onClick={() => toggleFlow(f.id)}
                title={(flowVisible[f.id] ? "Hide " : "Show ") + f.label + " lines"}>
                <span className="lg-line solid" style={{ borderTopColor: f.color }} />{f.short}
              </button>
            ))}
          </div>
          <div className="legend-hint">
            Hover to trace · Click for details · Open a meeting to Edit
          </div>
        </div>
      </header>

      <Canvas
        meetings={meetings} edges={edges} byId={byId} trace={trace}
        flowVisible={flowVisible}
        hoverId={hoverId} selectedId={selectedId}
        setHoverId={setHoverId} setSelectedId={setSelectedId} />

      <Drawer m={sel} byId={byId} edges={edges}
        editing={editing} setEditing={setEditing}
        onClose={() => setSelectedId(null)} onJump={id => setSelectedId(id)}
        onDelete={deleteMeeting}
        updateMeeting={updateMeeting} updateEdgeLabel={updateEdgeLabel} />

      {addingMeeting && <AddMeetingModal onSave={addMeeting} onClose={() => setAddingMeeting(false)} />}

      <NotesPanel open={notesOpen} value={notes} onChange={updateNotes} onClose={() => setNotesOpen(false)} />
    </div>
  );
}

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