/* Org Map - node-link ecosystem graph.
   Three comparable layouts: clustered domains, radial (funder-centric), organic.
   Solid edge = funded, dashed = not funded. Node shading = priority/need.
   Exports: OrgMap, computePositions (window).                          */

const WORLD = { w: 1520, h: 1060 };
const CENTER = { x: 760, y: 528 };

function orgSize(o) {
  return o.priority === "key" ? { w: 150, h: 60 } : { w: 132, h: 52 };
}

function computePositions(layout, orgs, room) {
  const D = window.DATA;
  const pos = {}; // id -> {x,y}
  const labels = []; // domain labels
  const byDomain = {};
  const S = room ? 1.42 : 1; // extra spacing when ID cards (people) are shown
  D.domains.forEach((d) => (byDomain[d.id] = []));
  orgs.forEach((o) => byDomain[o.domain].push(o));

  if (layout === "clustered") {
    const colX = [350, 760, 1170].map((x) => CENTER.x + (x - CENTER.x) * (room ? 1.5 : 1));
    const rowY = room ? [560, 1380] : [360, 720];
    D.domains.forEach((dom, di) => {
      const cx = colX[di % 3], cy = rowY[Math.floor(di / 3)];
      const list = byDomain[dom.id];
      const cols = 2, cellW = room ? 210 : 156, cellH = room ? 232 : 82;
      const rows = Math.ceil(list.length / cols);
      const gw = cols * cellW;
      list.forEach((o, j) => {
        const r = Math.floor(j / cols), c = j % cols;
        const rowCount = (r === rows - 1) ? (list.length - r * cols) : cols;
        const rowW = rowCount * cellW;
        pos[o.id] = {
          x: cx - rowW / 2 + cellW / 2 + c * cellW,
          y: cy - (rows * cellH) / 2 + cellH / 2 + r * cellH + 16,
        };
      });
      labels.push({ domain: dom.id, label: dom.label, x: cx, y: cy - (rows * cellH) / 2 - 18, anchor: "middle" });
    });
  } else if (layout === "radial") {
    pos[D.funder.id] = { x: CENTER.x, y: CENTER.y };
    D.domains.forEach((dom, di) => {
      const theta = -Math.PI / 2 + di * (Math.PI / 3);
      const list = byDomain[dom.id];
      list.forEach((o, j) => {
        const off = (j - (list.length - 1) / 2) * (room ? 0.46 : 0.40);
        const r = (248 + (j % 2) * 132) * S;
        const a = theta + off;
        pos[o.id] = { x: CENTER.x + Math.cos(a) * r * 1.18, y: CENTER.y + Math.sin(a) * r * 0.96 };
      });
      labels.push({
        domain: dom.id, label: dom.label,
        x: CENTER.x + Math.cos(theta) * 500 * 1.18 * S,
        y: CENTER.y + Math.sin(theta) * 470 * 0.96 * S,
        anchor: Math.abs(Math.cos(theta)) < 0.3 ? "middle" : (Math.cos(theta) > 0 ? "start" : "end"),
      });
    });
  } else { // organic
    pos[D.funder.id] = { x: CENTER.x, y: CENTER.y };
    const ga = 2.399963;
    orgs.forEach((o, j) => {
      const dom = D.domains.findIndex((d) => d.id === o.domain);
      const domTheta = -Math.PI / 2 + dom * (Math.PI / 3);
      const ang = j * ga;
      const rad = (150 + Math.sqrt(j + 0.6) * 74) * S;
      // blend scatter angle toward domain direction
      const bx = Math.cos(ang), by = Math.sin(ang);
      const dx = Math.cos(domTheta), dy = Math.sin(domTheta);
      const mx = bx * 0.62 + dx * 0.55, my = by * 0.62 + dy * 0.55;
      const m = Math.hypot(mx, my) || 1;
      pos[o.id] = { x: CENTER.x + (mx / m) * rad * 1.2, y: CENTER.y + (my / m) * rad * 0.92 };
    });
  }
  // projects placed relative to parent org
  return { pos, labels };
}

function projectPositions(layout, visibleOrgIds, pos) {
  const D = window.DATA;
  const ppos = {};
  visibleOrgIds.forEach((oid) => {
    const op = pos[oid];
    if (!op) return;
    const prjs = D.projectsByOrg(oid);
    prjs.forEach((pr, k) => {
      let ang;
      if (layout === "radial" || layout === "organic") {
        const base = Math.atan2(op.y - CENTER.y, op.x - CENTER.x);
        ang = base + (k - (prjs.length - 1) / 2) * 0.5;
        ppos[pr.id] = { x: op.x + Math.cos(ang) * 96, y: op.y + Math.sin(ang) * 78 };
      } else {
        ang = Math.PI / 2 + (k - (prjs.length - 1) / 2) * 0.7;
        ppos[pr.id] = { x: op.x + Math.cos(ang) * 70, y: op.y + 44 + k * 30 };
      }
    });
  });
  return ppos;
}

// Place each visible org's contact person as an ID card offset from the org.
function peoplePositions(layout, visibleOrgIds, pos) {
  const D = window.DATA;
  const out = {};
  const placed = new Set();
  visibleOrgIds.forEach((oid) => {
    const op = pos[oid];
    if (!op) return;
    const o = D.byId[oid];
    const pid = o.contact;
    if (!pid || placed.has(pid)) return;
    placed.add(pid);
    if (layout === "clustered") {
      out[pid] = { x: op.x, y: op.y + 100 }; // directly below the org, in the cell gap
    } else {
      // push straight outward, away from the funder at centre
      const dx = op.x - CENTER.x, dy = op.y - CENTER.y;
      const len = Math.hypot(dx, dy) || 1;
      out[pid] = { x: op.x + (dx / len) * 124, y: op.y + (dy / len) * 124 };
    }
  });
  return out;
}

// Place each visible event near the centroid of the orgs attending it.
function eventsPositions(layout, visibleOrgIds, pos) {
  const D = window.DATA;
  const out = {};
  const visSet = new Set(visibleOrgIds);
  D.events.forEach((ev, idx) => {
    const orgPts = (ev.orgs || []).filter((o) => visSet.has(o)).map((o) => pos[o]).filter(Boolean);
    if (!orgPts.length) return;
    let cx = 0, cy = 0;
    orgPts.forEach((p) => { cx += p.x; cy += p.y; });
    cx /= orgPts.length; cy /= orgPts.length;
    if (layout === "clustered") {
      out[ev.id] = { x: cx, y: cy - 120 };
    } else {
      // push outward from centre so events ring the ecosystem
      const dx = cx - CENTER.x, dy = cy - CENTER.y;
      const len = Math.hypot(dx, dy) || 1;
      out[ev.id] = { x: cx + (dx / len) * 150, y: cy + (dy / len) * 150 };
    }
  });
  return out;
}

function OrgMap({ theme, tweaks, onSelect, selectedId, showProjects, setShowProjects, showPeople, setShowPeople, showEvents, setShowEvents, layout, setLayout, onOpenIntel }) {
  const D = window.DATA;
  const [filters, setFilters] = React.useState({ reach: "all", funding: "all", relationship: "all", priority: "all" });
  const [mapView, setMapView] = React.useState("graph"); // "graph" = cluster map · "nexus" = 3D funding map
  const [hoverId, setHoverId] = React.useState(null);
  const [filtersOpen, setFiltersOpen] = React.useState(true);

  // viewport transform
  const wrapRef = React.useRef(null);
  const boundsRef = React.useRef({ x: 0, y: 0, w: WORLD.w, h: WORLD.h });
  const [view, setView] = React.useState({ x: 0, y: 0, k: 0.92 });
  const drag = React.useRef(null);

  const fitToWrap = React.useCallback(() => {
    const el = wrapRef.current; if (!el) return;
    const r = el.getBoundingClientRect();
    const b = boundsRef.current;
    const padL = 256, padR = 80, padT = 28, padB = 110; // clear filter rail + legend/zoom
    const availW = Math.max(200, r.width - padL - padR);
    const availH = Math.max(200, r.height - padT - padB);
    const k = Math.min(availW / b.w, availH / b.h) * 0.96;
    setView({ k, x: padL + (availW - b.w * k) / 2 - b.x * k, y: padT + (availH - b.h * k) / 2 - b.y * k });
  }, []);

  const visibleOrgs = D.orgs.filter((o) => {
    if (filters.reach !== "all" && o.reach !== filters.reach) return false;
    if (filters.funding === "funded" && !o.funded) return false;
    if (filters.funding === "unfunded" && o.funded) return false;
    if (filters.relationship !== "all" && o.relationship !== filters.relationship) return false;
    if (filters.priority === "key" && o.priority !== "key") return false;
    if (filters.priority === "need" && o.priority !== "need") return false;
    return true;
  });
  const visibleIds = new Set(visibleOrgs.map((o) => o.id));

  const { pos, labels } = React.useMemo(() => computePositions(layout, visibleOrgs, showPeople), [layout, visibleOrgs.length, JSON.stringify(filters), showPeople]);
  const ppos = React.useMemo(() => showProjects ? projectPositions(layout, [...visibleIds], pos) : {}, [layout, showProjects, pos]);
  const pplpos = React.useMemo(() => showPeople ? peoplePositions(layout, [...visibleIds], pos) : {}, [layout, showPeople, pos]);
  const epos = React.useMemo(() => showEvents ? eventsPositions(layout, [...visibleIds], pos) : {}, [layout, showEvents, pos]);

  // content bounds (so fit uses real extent, not the fixed world)
  const bounds = React.useMemo(() => {
    const all = [...Object.values(pos), ...Object.values(ppos), ...Object.values(pplpos), ...Object.values(epos)];
    if (!all.length) return { x: 0, y: 0, w: WORLD.w, h: WORLD.h };
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    all.forEach((p) => { minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y); });
    const m = 110; // node half-size + label margin
    return { x: minX - m, y: minY - m - 18, w: (maxX - minX) + m * 2, h: (maxY - minY) + m * 2 + 18 };
  }, [pos, ppos, pplpos, epos]);
  React.useEffect(() => { boundsRef.current = bounds; fitToWrap(); }, [bounds, fitToWrap]);

  // edges
  const showFunderEdges = layout === "radial" || layout === "organic";
  const funderEdges = showFunderEdges ? visibleOrgs.map((o) => ({ a: D.funder.id, b: o.id, funded: o.funded })) : [];
  const linkedEdges = [];
  if (layout === "clustered" || layout === "organic") {
    const seen = new Set();
    visibleOrgs.forEach((o) => (o.linked || []).forEach((l) => {
      if (!visibleIds.has(l)) return;
      const key = [o.id, l].sort().join("|");
      if (seen.has(key)) return; seen.add(key);
      linkedEdges.push({ a: o.id, b: l });
    }));
  }
  const projEdges = showProjects ? D.projects.filter((p) => visibleIds.has(p.org) && ppos[p.id]).map((p) => ({ a: p.org, b: p.id })) : [];
  const peopleEdges = showPeople ? D.orgs.filter((o) => visibleIds.has(o.id) && o.contact && pplpos[o.contact]).map((o) => ({ a: o.id, b: o.contact })) : [];
  const eventEdges = showEvents ? D.events.flatMap((ev) => epos[ev.id] ? (ev.orgs || []).filter((o) => visibleIds.has(o)).map((o) => ({ a: o, b: ev.id })) : []) : [];

  // neighbors for hover dim
  const neighborsOf = (id) => {
    const s = new Set([id]);
    [...funderEdges, ...linkedEdges, ...projEdges, ...peopleEdges, ...eventEdges].forEach((e) => {
      if (e.a === id) s.add(e.b); if (e.b === id) s.add(e.a);
    });
    return s;
  };
  const active = hoverId || selectedId;
  const hi = active ? neighborsOf(active) : null;

  // pan/zoom handlers
  const onWheel = (e) => {
    e.preventDefault();
    const el = wrapRef.current.getBoundingClientRect();
    const mx = e.clientX - el.left, my = e.clientY - el.top;
    const factor = Math.exp(-e.deltaY * 0.0014);
    setView((v) => {
      const k = Math.min(2.4, Math.max(0.4, v.k * factor));
      const ratio = k / v.k;
      return { k, x: mx - (mx - v.x) * ratio, y: my - (my - v.y) * ratio };
    });
  };
  const onDown = (e) => {
    if (e.target.closest(".node")) return;
    drag.current = { sx: e.clientX, sy: e.clientY, vx: view.x, vy: view.y };
  };
  const onMove = (e) => {
    if (!drag.current) return;
    setView((v) => ({ ...v, x: drag.current.vx + (e.clientX - drag.current.sx), y: drag.current.vy + (e.clientY - drag.current.sy) }));
  };
  const onUp = () => (drag.current = null);
  const zoom = (dir) => setView((v) => {
    const k = Math.min(2.4, Math.max(0.4, v.k * (dir > 0 ? 1.2 : 1 / 1.2)));
    const el = wrapRef.current.getBoundingClientRect();
    const mx = el.width / 2, my = el.height / 2, ratio = k / v.k;
    return { k, x: mx - (mx - v.x) * ratio, y: my - (my - v.y) * ratio };
  });

  const edgeColor = "var(--border-strong)";
  const P = (id) => pos[id] || ppos[id] || pplpos[id] || epos[id];
  function edgePath(a, b) {
    const pa = P(a), pb = P(b);
    if (!pa || !pb) return null;
    const mx = (pa.x + pb.x) / 2, my = (pa.y + pb.y) / 2;
    const dx = pb.x - pa.x, dy = pb.y - pa.y;
    const curve = layout === "clustered" ? 0 : 0.12;
    const cx = mx - dy * curve, cy = my + dx * curve;
    return `M ${pa.x} ${pa.y} Q ${cx} ${cy} ${pb.x} ${pb.y}`;
  }

  const filterDefs = [
    { key: "reach", label: "Reach", opts: [["all", "All"], ["Local", "Local"], ["National", "National"], ["Global", "Global"]] },
    { key: "funding", label: "Funding", opts: [["all", "All"], ["funded", "Funded"], ["unfunded", "Not funded"]] },
    { key: "relationship", label: "Relationship", opts: [["all", "All"], ["Grantee", "Grantee"], ["Active supporter", "Supporter"], ["Prospect", "Prospect"]] },
    { key: "priority", label: "Priority", opts: [["all", "All"], ["key", "Key org"], ["need", "High need"]] },
  ];
  const anyFilter = Object.values(filters).some((v) => v !== "all");

  return (
    <div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column" }}>
      {/* toolbar */}
      <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "14px 24px", borderBottom: "1px solid var(--border)", background: "var(--surface)", flexWrap: "wrap", zIndex: 3 }}>
        <div>
          <div style={{ fontSize: 17, fontWeight: 650, letterSpacing: "-0.01em" }}>Map</div>
          <div style={{ fontSize: 12.5, color: "var(--text-muted)" }}>
            {mapView === "nexus"
              ? <span>Live 3D funding map · drag to orbit, scroll to zoom</span>
              : <span>
                  <span className="mono">{visibleOrgs.length}</span> of <span className="mono">{D.orgs.length}</span> organisations
                  {showProjects && <span> · <span className="mono">{projEdges.length}</span> projects</span>}
                  {showPeople && <span> · <span className="mono">{peopleEdges.length}</span> people</span>}
                  {showEvents && <span> · <span className="mono">{Object.keys(epos).length}</span> events</span>}
                </span>}
          </div>
        </div>
        <Segmented value={mapView} onChange={setMapView} options={[
          { value: "graph", label: "Cluster Map", icon: "grid" },
          { value: "nexus", label: "Nexus 3D", icon: "radial" },
        ]} />
        <div style={{ flex: 1 }} />
        {mapView === "graph" && <React.Fragment>
          <Segmented value={layout} onChange={setLayout} options={[
            { value: "clustered", label: "Clustered", icon: "grid" },
            { value: "radial", label: "Radial", icon: "radial" },
            { value: "organic", label: "Organic", icon: "organic" },
          ]} />
          <button className="btn btn-sm" onClick={() => setShowPeople(!showPeople)}
            style={showPeople ? { background: "var(--accent-soft)", borderColor: "var(--accent)", color: "var(--accent)" } : {}}>
            <Icon name="directory" size={15} /> People
          </button>
          <button className="btn btn-sm" onClick={() => setShowProjects(!showProjects)}
            style={showProjects ? { background: "var(--accent-soft)", borderColor: "var(--accent)", color: "var(--accent)" } : {}}>
            <Icon name={showProjects ? "eye" : "eyeOff"} size={15} /> Projects
          </button>
          <button className="btn btn-sm" onClick={() => setShowEvents(!showEvents)}
            style={showEvents ? { background: "var(--accent-soft)", borderColor: "var(--accent)", color: "var(--accent)" } : {}}>
            <Icon name="calendar" size={15} /> Events
          </button>
          <button className="btn btn-sm btn-primary" onClick={() => onOpenIntel && onOpenIntel(selectedId)} title="Open the Intel briefing board">
            <Icon name="target" size={15} /> Open Intel
          </button>
        </React.Fragment>}
      </div>

      {mapView === "nexus" && (
        <iframe title="Nexus 3D funding map" src="nexus/nexus.html"
          style={{ flex: 1, width: "100%", border: "none", background: "#f3f0e8", minHeight: 0, display: "block" }} />
      )}

      <div style={{ position: "relative", flex: 1, minHeight: 0, display: mapView === "nexus" ? "none" : "block" }}>
        {/* filter rail */}
        <div style={{ position: "absolute", top: 16, left: 16, zIndex: 4, width: filtersOpen ? 224 : "auto" }}>
          <div className="card" style={{ padding: filtersOpen ? 14 : 8, boxShadow: "var(--shadow)" }}>
            <button className="tap focusable" onClick={() => setFiltersOpen(!filtersOpen)}
              style={{ display: "flex", alignItems: "center", gap: 8, width: "100%", background: "none", border: "none", cursor: "pointer", color: "var(--text)", font: "inherit", padding: 0 }}>
              <Icon name="filter" size={15} style={{ color: "var(--accent)" }} />
              <span style={{ fontWeight: 600, fontSize: 13 }}>Filters</span>
              {anyFilter && <span className="badge" style={{ background: "var(--accent-soft)", color: "var(--accent)", borderColor: "transparent", padding: "2px 7px" }}>on</span>}
              <span style={{ flex: 1 }} />
              <Icon name="chevronDown" size={15} style={{ transform: filtersOpen ? "none" : "rotate(-90deg)", color: "var(--text-muted)" }} />
            </button>
            {filtersOpen && (
              <div style={{ marginTop: 12, display: "flex", flexDirection: "column", gap: 13 }}>
                {filterDefs.map((f) => (
                  <div key={f.key}>
                    <div style={{ fontSize: 11, fontWeight: 600, color: "var(--text-muted)", marginBottom: 6 }}>{f.label}</div>
                    <div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>
                      {f.opts.map(([v, lbl]) => {
                        const on = filters[f.key] === v;
                        return (
                          <button key={v + (on ? "-on" : "-off")} className="tap focusable" onClick={() => setFilters((s) => ({ ...s, [f.key]: v }))}
                            style={{
                              font: "inherit", fontSize: 11.5, fontWeight: 550, cursor: "pointer",
                              padding: "4px 9px", borderRadius: 999,
                              border: "1px solid " + (on ? "transparent" : "var(--border)"),
                              background: on ? "var(--accent)" : "var(--surface-2)",
                              color: on ? "var(--accent-contrast)" : "var(--text-muted)",
                            }}>{lbl}</button>
                        );
                      })}
                    </div>
                  </div>
                ))}
                {anyFilter && (
                  <button className="btn btn-sm btn-ghost" style={{ justifyContent: "center", color: "var(--accent)" }}
                    onClick={() => setFilters({ reach: "all", funding: "all", relationship: "all", priority: "all" })}>
                    Clear filters
                  </button>
                )}
              </div>
            )}
          </div>
        </div>

        {/* legend */}
        <div style={{ position: "absolute", bottom: 16, left: 16, zIndex: 4 }}>
          <div className="card" style={{ padding: "11px 13px", display: "flex", flexDirection: "column", gap: 8, boxShadow: "var(--shadow)" }}>
            <div className="t-eyebrow">Legend</div>
            <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12, color: "var(--text-muted)" }}>
              <svg width="34" height="8"><line x1="1" y1="4" x2="33" y2="4" stroke="var(--text-muted)" strokeWidth="2" /></svg> Funded
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12, color: "var(--text-muted)" }}>
              <svg width="34" height="8"><line x1="1" y1="4" x2="33" y2="4" stroke="var(--text-faint)" strokeWidth="2" strokeDasharray="4 3" /></svg> Not funded
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12, color: "var(--text-muted)" }}>
              <span style={{ width: 16, height: 12, borderRadius: 4, background: "var(--accent-soft-2)", border: "1px solid var(--accent)" }} /> Key org
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12, color: "var(--text-muted)" }}>
              <span style={{ width: 16, height: 12, borderRadius: 4, background: "var(--rose-bg)", border: "1px solid var(--rose)" }} /> High need
            </div>
          </div>
        </div>

        {/* zoom controls */}
        <div style={{ position: "absolute", bottom: 16, right: 16, zIndex: 4, display: "flex", flexDirection: "column", gap: 6 }}>
          <button className="btn btn-icon" onClick={() => zoom(1)} title="Zoom in"><Icon name="zoomIn" size={16} /></button>
          <button className="btn btn-icon" onClick={() => zoom(-1)} title="Zoom out"><Icon name="zoomOut" size={16} /></button>
          <button className="btn btn-icon" onClick={fitToWrap} title="Recenter"><Icon name="recenter" size={16} /></button>
        </div>

        {/* canvas */}
        <div ref={wrapRef} className="grid-texture"
          onWheel={onWheel} onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={onUp}
          style={{ position: "absolute", inset: 0, overflow: "hidden", cursor: drag.current ? "grabbing" : "grab", background: "var(--bg)" }}>
          <div style={{ position: "absolute", top: 0, left: 0, width: WORLD.w, height: WORLD.h, transformOrigin: "0 0", transform: `translate(${view.x}px,${view.y}px) scale(${view.k})` }}>
            {/* edges */}
            <svg width={WORLD.w} height={WORLD.h} style={{ position: "absolute", inset: 0, overflow: "visible", pointerEvents: "none" }}>
              {funderEdges.map((e, i) => {
                const d = edgePath(e.a, e.b); if (!d) return null;
                const dim = hi && !(hi.has(e.a) && hi.has(e.b));
                return <path key={"f" + i} d={d} fill="none"
                  stroke={e.funded ? "var(--accent)" : "var(--text-faint)"}
                  strokeWidth={e.funded ? 1.7 : 1.4} strokeDasharray={e.funded ? "none" : "5 4"}
                  opacity={dim ? 0.12 : (e.funded ? 0.5 : 0.42)} />;
              })}
              {linkedEdges.map((e, i) => {
                const d = edgePath(e.a, e.b); if (!d) return null;
                const dim = hi && !(hi.has(e.a) && hi.has(e.b));
                return <path key={"l" + i} d={d} fill="none" stroke={edgeColor} strokeWidth="1.4" opacity={dim ? 0.1 : 0.55} />;
              })}
              {projEdges.map((e, i) => {
                const d = edgePath(e.a, e.b); if (!d) return null;
                const dim = hi && !(hi.has(e.a) && hi.has(e.b));
                return <path key={"p" + i} d={d} fill="none" stroke="var(--text-faint)" strokeWidth="1.2" opacity={dim ? 0.1 : 0.4} strokeDasharray="2 3" />;
              })}
              {peopleEdges.map((e, i) => {
                const d = edgePath(e.a, e.b); if (!d) return null;
                const dim = hi && !(hi.has(e.a) && hi.has(e.b));
                return <path key={"pe" + i} d={d} fill="none" stroke="var(--accent)" strokeWidth="1.3" opacity={dim ? 0.12 : 0.42} />;
              })}
              {eventEdges.map((e, i) => {
                const d = edgePath(e.a, e.b); if (!d) return null;
                const dim = hi && !(hi.has(e.a) && hi.has(e.b));
                return <path key={"ev" + i} d={d} fill="none" stroke="var(--warn)" strokeWidth="1.2" opacity={dim ? 0.1 : 0.34} strokeDasharray="1 4" strokeLinecap="round" />;
              })}
            </svg>

            {/* domain labels (clustered/radial) */}
            {labels.map((l) => (
              <div key={l.label} style={{
                position: "absolute", left: l.x, top: l.y, transform: `translate(${l.anchor === "middle" ? "-50%" : l.anchor === "end" ? "-100%" : "0"}, -50%)`,
                fontSize: 12, fontWeight: 600, letterSpacing: "0.04em", textTransform: "uppercase",
                color: domainColor(l.domain), opacity: 0.85, whiteSpace: "nowrap", pointerEvents: "none",
              }}>
                <span style={{ display: "inline-flex", alignItems: "center", gap: 7 }}>
                  <span style={{ width: 8, height: 8, borderRadius: 999, background: domainColor(l.domain) }} />
                  {l.label}
                </span>
              </div>
            ))}

            {/* funder node */}
            {pos[D.funder.id] && (() => {
              const p = pos[D.funder.id];
              const dim = hi && !hi.has(D.funder.id);
              return (
                <div className="node" onClick={() => onSelect(D.funder.id)} onMouseEnter={() => setHoverId(D.funder.id)} onMouseLeave={() => setHoverId(null)}
                  style={{
                    position: "absolute", left: p.x, top: p.y, transform: "translate(-50%,-50%)",
                    width: 132, padding: "13px 16px", borderRadius: 999, cursor: "pointer",
                    background: "var(--accent)", color: "var(--accent-contrast)",
                    boxShadow: selectedId === D.funder.id ? "0 0 0 4px var(--accent-ring), var(--shadow-lg)" : "var(--shadow-lg)",
                    textAlign: "center", opacity: dim ? 0.35 : 1, zIndex: 2,
                  }}>
                  <div style={{ fontSize: 10.5, opacity: 0.8, fontWeight: 600, letterSpacing: "0.05em", textTransform: "uppercase" }}>Funder</div>
                  <div style={{ fontWeight: 650, fontSize: 13.5, lineHeight: 1.2 }}>{D.funder.name}</div>
                </div>
              );
            })()}

            {/* project nodes */}
            {showProjects && D.projects.filter((p) => ppos[p.id]).map((pr) => {
              const p = ppos[pr.id];
              const dim = hi && !hi.has(pr.id);
              const sel = selectedId === pr.id;
              return (
                <div key={pr.id} className="node" onClick={() => onSelect(pr.id)} onMouseEnter={() => setHoverId(pr.id)} onMouseLeave={() => setHoverId(null)}
                  style={{
                    position: "absolute", left: p.x, top: p.y, transform: "translate(-50%,-50%)",
                    width: 124, padding: "6px 9px", borderRadius: 8, cursor: "pointer",
                    background: "var(--surface-2)", border: "1px dashed var(--border-strong)",
                    boxShadow: sel ? "0 0 0 3px var(--accent-ring)" : "var(--shadow-sm)",
                    opacity: dim ? 0.3 : 1, zIndex: 1,
                  }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 5 }}>
                    <Icon name="folder" size={11} style={{ color: "var(--text-faint)" }} />
                    <span style={{ fontSize: 10.5, color: "var(--text-faint)", fontWeight: 600 }} className="mono">{pr.type}</span>
                  </div>
                  <div style={{ fontSize: 11, fontWeight: 550, lineHeight: 1.2, marginTop: 2, color: "var(--text-muted)", overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical" }}>
                    {pr.name.replace(/^\d{4}\s\w+:\s/, "")}
                  </div>
                </div>
              );
            })}

            {/* event nodes */}
            {showEvents && D.events.filter((ev) => epos[ev.id]).map((ev) => {
              const p = epos[ev.id];
              const dim = hi && !hi.has(ev.id);
              const sel = selectedId === ev.id;
              const d = new Date(ev.date);
              return (
                <div key={ev.id} className="node" onClick={() => onSelect(ev.id)} onMouseEnter={() => setHoverId(ev.id)} onMouseLeave={() => setHoverId(null)}
                  style={{
                    position: "absolute", left: p.x, top: p.y, transform: `translate(-50%,-50%) scale(${sel || hoverId === ev.id ? 1.05 : 1})`,
                    display: "flex", alignItems: "center", gap: 8, width: 150, padding: "7px 10px", borderRadius: 999, cursor: "pointer",
                    background: "var(--warn-bg)", border: "1.5px solid var(--warn)",
                    boxShadow: sel ? "0 0 0 3px var(--accent-ring), var(--shadow)" : (dim ? "none" : "var(--shadow-sm)"),
                    opacity: dim ? 0.3 : 1, zIndex: sel ? 3 : 1,
                  }}>
                  <div style={{ width: 26, flex: "none", textAlign: "center", lineHeight: 1 }}>
                    <div className="mono" style={{ fontSize: 13, fontWeight: 700, color: "var(--warn)" }}>{d.getDate()}</div>
                    <div style={{ fontSize: 8.5, color: "var(--warn)", textTransform: "uppercase", fontWeight: 600 }}>{d.toLocaleDateString("en-GB", { month: "short" })}</div>
                  </div>
                  <div style={{ minWidth: 0, flex: 1 }}>
                    <div style={{ fontSize: 11, fontWeight: 600, lineHeight: 1.2, color: "var(--text)", overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical" }}>{ev.name}</div>
                  </div>
                </div>
              );
            })}

            {/* person ID-card nodes */}
            {showPeople && Object.keys(pplpos).map((pid) => {
              const p = pplpos[pid];
              const dim = hi && !hi.has(pid);
              const sel = selectedId === pid;
              const hov = hoverId === pid;
              return (
                <div key={pid} className="node" onClick={() => onSelect(pid)} onMouseEnter={() => setHoverId(pid)} onMouseLeave={() => setHoverId(null)}
                  style={{
                    position: "absolute", left: p.x, top: p.y, cursor: "pointer",
                    transform: `translate(-50%,-50%) scale(${sel || hov ? 1.04 : 1})`,
                    opacity: dim ? 0.32 : 1, zIndex: sel ? 3 : 2,
                  }}>
                  <IDCard id={pid} width={176} selected={sel} photoSize={38} />
                </div>
              );
            })}

            {/* org nodes */}
            {visibleOrgs.map((o) => {
              const p = pos[o.id]; if (!p) return null;
              const sz = orgSize(o);
              const dim = hi && !hi.has(o.id);
              const sel = selectedId === o.id;
              const dc = domainColor(o.domain);
              let bg = "var(--surface)", border = "var(--border-strong)";
              if (o.priority === "key") { bg = "var(--accent-soft)"; border = "var(--accent)"; }
              else if (o.priority === "need") { bg = "var(--rose-bg)"; border = "var(--rose)"; }
              return (
                <div key={o.id} className="node" onClick={() => onSelect(o.id)} onMouseEnter={() => setHoverId(o.id)} onMouseLeave={() => setHoverId(null)}
                  style={{
                    position: "absolute", left: p.x, top: p.y, transform: "translate(-50%,-50%)",
                    width: sz.w, minHeight: sz.h, padding: "9px 11px", borderRadius: 11, cursor: "pointer",
                    background: bg,
                    border: `${o.funded ? "1.5px solid" : "1.5px dashed"} ${border}`,
                    boxShadow: sel ? "0 0 0 4px var(--accent-ring), var(--shadow-lg)" : (dim ? "none" : "var(--shadow)"),
                    opacity: dim ? 0.32 : 1, zIndex: sel ? 3 : 2,
                    transform: `translate(-50%,-50%) scale(${sel || hoverId === o.id ? 1.04 : 1})`,
                  }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }}>
                    <span style={{ width: 7, height: 7, borderRadius: 999, background: dc, flex: "none" }} />
                    {o.priority === "key" && <Icon name="spark" size={11} style={{ color: "var(--accent)" }} />}
                    {o.priority === "need" && <span style={{ fontSize: 9.5, fontWeight: 700, color: "var(--rose)" }}>NEED</span>}
                  </div>
                  <div style={{ fontSize: 12.5, fontWeight: 600, lineHeight: 1.2, color: "var(--text)" }}>{o.name}</div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
}

window.OrgMap = OrgMap;
window.computePositions = computePositions;
