/* ============================================================================
   DIVONCI WORLD — PRIMITIVES & TELEMETRY
   Icons, state chips, rings, gauges, sparklines, micro-bars, and the
   canvas-rendered Empire Map. Exported to window for cross-file use.
   ========================================================================== */
const { useRef, useEffect, useState, useMemo } = React;

/* ----------------------------------------------------------------- ICONS - */
/* Line icons, 1.6 stroke, round caps — per design system icon rules.        */
const ICON_PATHS = {
  globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.6 2.5 15.4 0 18M12 3c-2.5 2.6-2.5 15.4 0 18"/>',
  council: '<circle cx="12" cy="7" r="3"/><circle cx="5.5" cy="16" r="2.5"/><circle cx="18.5" cy="16" r="2.5"/><path d="M9.7 9.2 6.8 13.6M14.3 9.2l2.9 4.4M8 16h8"/>',
  agent: '<circle cx="12" cy="8" r="4"/><path d="M5 20c0-3.3 3.1-6 7-6s7 2.7 7 6"/>',
  nexus: '<circle cx="6" cy="6" r="2"/><circle cx="18" cy="7" r="2"/><circle cx="7" cy="18" r="2"/><circle cx="17" cy="17" r="2"/><circle cx="12" cy="12" r="2.4"/><path d="M7.6 7.4 10.4 10.4M16.4 8.2 13.6 10.6M8.2 16.6 10.6 13.6M15.6 15.8 13.4 13.4"/>',
  project: '<rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M3.5 9h17M8 4.5v15"/>',
  warroom: '<path d="M12 3 4 7v5c0 4.4 3.2 7.6 8 9 4.8-1.4 8-4.6 8-9V7z"/><path d="M9.3 12.2 11.2 14 15 10"/>',
  workrate: '<path d="M4 19V5M4 19h16"/><rect x="7" y="11" width="3" height="5"/><rect x="12" y="7" width="3" height="9"/><rect x="17" y="13" width="3" height="3"/>',
  studio: '<rect x="3.5" y="5" width="17" height="12" rx="2"/><path d="M3.5 17l4-5 3 3 3-4 4 6M12 21h0M8.5 21h7"/>',
  foundry: '<path d="M5 21h14M6 21v-7l-2-2 4-2 2 2h4l2-2 4 2-2 2v7"/><path d="M9 21v-4h6v4"/>',
  evidence: '<path d="M7 3h7l5 5v13H7z" /><path d="M14 3v5h5M9.5 13l1.8 1.8L15 11"/>',
  risk: '<path d="M12 3 2.5 20.5h19z"/><path d="M12 10v5M12 17.6h0"/>',
  comms: '<path d="M4 5h16v11H8l-4 3z"/><path d="M8 9h8M8 12h5"/>',
  finance: '<circle cx="12" cy="12" r="9"/><path d="M9.5 9.5c0-1.2 1.1-2 2.5-2s2.5.8 2.5 2-1.1 1.7-2.5 2.2S9.5 14 9.5 15s1.1 2 2.5 2 2.5-.8 2.5-2M12 6v1.5M12 16.5V18"/>',
  ops: '<circle cx="12" cy="12" r="3"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1"/>',
  settings: '<circle cx="12" cy="12" r="3"/><path d="M19.4 12a7.4 7.4 0 0 0-.1-1.2l2-1.5-2-3.4-2.3 1a7 7 0 0 0-2-1.2l-.4-2.5H10.4l-.4 2.5a7 7 0 0 0-2 1.2l-2.3-1-2 3.4 2 1.5a7.4 7.4 0 0 0 0 2.4l-2 1.5 2 3.4 2.3-1a7 7 0 0 0 2 1.2l.4 2.5h3.2l.4-2.5a7 7 0 0 0 2-1.2l2.3 1 2-3.4-2-1.5c.06-.4.1-.8.1-1.2Z"/>',
  skill: '<circle cx="6" cy="12" r="2.2"/><circle cx="18" cy="6" r="2.2"/><circle cx="18" cy="18" r="2.2"/><path d="M8 11 16 6.8M8 13l8 4.2"/>',
  log: '<path d="M5 4h14v16H5z"/><path d="M8 8h8M8 12h8M8 16h5"/>',
  search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.2-3.2"/>',
  bell: '<path d="M18 8a6 6 0 1 0-12 0c0 7-3 8-3 8h18s-3-1-3-8"/><path d="M10.5 21a2 2 0 0 0 3 0"/>',
  bolt: '<path d="M13 2 4 14h7l-1 8 9-12h-7z"/>',
  chevron: '<path d="m9 6 6 6-6 6"/>',
  arrow: '<path d="M5 12h14M13 6l6 6-6 6"/>',
  crown: '<path d="M4 8l3.5 4L12 6l4.5 6L20 8l-1.5 11h-13z"/>',
  shield: '<path d="M12 3 4 6v6c0 4.4 3.2 7.6 8 9 4.8-1.4 8-4.6 8-9V6z"/>',
  spark: '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2.5 2.5M15.5 15.5 18 18M18 6l-2.5 2.5M8.5 15.5 6 18"/><circle cx="12" cy="12" r="2.4"/>',
  mask: '<path d="M4 6c4-1 12-1 16 0 0 7-2 12-8 12S4 13 4 6Z"/><path d="M9 11h.01M15 11h.01M9.5 15c1.5 1 3.5 1 5 0"/>',
  eye: '<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6S2 12 2 12Z"/><circle cx="12" cy="12" r="2.6"/>',
  power: '<path d="M12 4v8M7 7a7 7 0 1 0 10 0"/>',
  filter: '<path d="M3 5h18l-7 8v6l-4-2v-4z"/>',
  plus: '<path d="M12 5v14M5 12h14"/>',
  check: '<path d="m5 12 4 4 10-11"/>',
  pause: '<path d="M8 5v14M16 5v14"/>',
  play: '<path d="M7 4l13 8-13 8z"/>',
  lock: '<rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/>',
  device: '<rect x="6" y="2.5" width="12" height="19" rx="2.5"/><path d="M10.5 5.5h3M10 18.5h4"/>',
  branch: '<circle cx="6" cy="5.5" r="2.2"/><circle cx="6" cy="18.5" r="2.2"/><circle cx="18" cy="6.5" r="2.2"/><path d="M6 7.7v8.6M8.2 5.8h5.2a3 3 0 0 1 3 3v.5M16.4 9.3c0 4.2-3.4 5.7-7 5.9"/>',
  rollback: '<path d="M5 9h9.5a5.5 5.5 0 1 1-5.5 5.5"/><path d="m5 9 3.5-3.5M5 9l3.5 3.5"/>',
  rocket: '<path d="M5 15c-1.5 1.5-2 5-2 5s3.5-.5 5-2M9 11a8 8 0 0 1 9-8c.5 4.5-1 7.5-4 9.5-2 1.3-4 1.5-4 1.5l-2.5-2.5S8 9.5 9 11Z"/><circle cx="14.5" cy="7.5" r="1.3"/>',
  close: '<path d="M6 6l12 12M18 6 6 18"/>',
  chip: '<rect x="6" y="6" width="12" height="12" rx="2"/><path d="M9.5 9.5h5v5h-5z"/><path d="M9 3v3M15 3v3M9 18v3M15 18v3M3 9h3M3 15h3M18 9h3M18 15h3"/>',
  code: '<path d="m8 7-5 5 5 5M16 7l5 5-5 5M13.5 4l-3 16"/>',
  layers: '<path d="M12 3 3 8l9 5 9-5z"/><path d="m3 13 9 5 9-5M3 16.5l9 5 9-5"/>',
  cube: '<path d="M12 3 4 7v10l8 4 8-4V7z"/><path d="M4 7l8 4 8-4M12 11v10"/>',
  film: '<rect x="3.5" y="4.5" width="17" height="15" rx="2"/><path d="M8 4.5v15M16 4.5v15M3.5 9.5h4.5M3.5 14.5h4.5M16 9.5h4.5M16 14.5h4.5"/>',
  database: '<ellipse cx="12" cy="5.5" rx="7" ry="3"/><path d="M5 5.5v6c0 1.7 3.1 3 7 3s7-1.3 7-3v-6M5 11.5v6c0 1.7 3.1 3 7 3s7-1.3 7-3v-6"/>',
  cloud: '<path d="M7 18a4 4 0 0 1-.5-7.97A6 6 0 0 1 18 9.5a3.5 3.5 0 0 1-.5 8.5z"/>',
  plug: '<path d="M9 3v5M15 3v5M6 8h12v3a6 6 0 0 1-12 0zM12 17v4"/>',
  card: '<rect x="3" y="5.5" width="18" height="13" rx="2"/><path d="M3 10h18M7 15h3"/>',
  chart: '<path d="M4 4v16h16"/><path d="M8 16v-4M12 16V8M16 16v-6"/>',
  beaker: '<path d="M9 3h6M10 3v6L5 19a1.5 1.5 0 0 0 1.4 2h11.2A1.5 1.5 0 0 0 19 19l-5-10V3"/><path d="M7.5 15h9"/>',
  mic: '<rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/>',
  refresh: '<path d="M20 11a8 8 0 1 0-1.5 5.5"/><path d="M20 5v6h-6"/>',
  users: '<circle cx="9" cy="8" r="3.2"/><path d="M3.5 20c0-3 2.5-5.2 5.5-5.2s5.5 2.2 5.5 5.2"/><path d="M16 5.5a3 3 0 0 1 0 5.8M16.5 14.6c2.6.4 4.5 2.4 4.5 5.1"/>',
  handshake: '<path d="m11 12 2 2 4-4 3 3-5 5-3-2-3 2-4-4 5-5z"/><path d="M3 11l4-4 3 1"/>',
  doc: '<path d="M7 3h7l4 4v14H7z"/><path d="M14 3v4h4M10 13h6M10 16.5h4"/>',
  invoice: '<path d="M6 3h12v18l-3-2-3 2-3-2-3 2z"/><path d="M9 8h6M9 11.5h6M9 15h3"/>',
  flag: '<path d="M5 21V4M5 5h11l-2 3 2 3H5"/>',
};
function Icon({ name, className = "glyph", style }) {
  return (
    <svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor"
      strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={style}
      dangerouslySetInnerHTML={{ __html: ICON_PATHS[name] || "" }} />
  );
}

/* ------------------------------------------------------------- STATE CHIP - */
function StateChip({ state, label, tag }) {
  const meta = DW.STATE_META[state] || DW.STATE_META.standby;
  return (
    <span className="sbadge" style={{ color: meta.c, borderColor: `color-mix(in srgb, ${meta.c} 40%, transparent)` }}>
      <span className="led" style={{ background: meta.c, color: meta.c }} />
      <span style={{ color: "var(--ink)" }}>{label || meta.label}</span>
      {tag && <span style={{ color: "var(--ink-4)" }}>· {tag}</span>}
    </span>
  );
}
function Led({ state, size = 8 }) {
  const c = (DW.STATE_META[state] || DW.STATE_META.standby).c;
  return <span style={{ width: size, height: size, borderRadius: 99, background: c, color: c, boxShadow: "0 0 8px currentColor", flex: "none", display: "inline-block" }} />;
}

/* ------------------------------------------------------------------ RING - */
function Ring({ p = 70, color = "var(--violet-bright)", size = 84, label, unit }) {
  const r = size / 2 - 6, c = 2 * Math.PI * r, off = c * (1 - p / 100);
  return (
    <div className="ring" style={{ width: size, height: size, position: "relative", flex: "none" }}>
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ transform: "rotate(-90deg)" }}>
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="var(--graphite)" strokeWidth="7" />
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke={color} strokeWidth="7" strokeLinecap="round"
          strokeDasharray={c} strokeDashoffset={off} style={{ filter: `drop-shadow(0 0 5px ${color})`, transition: "stroke-dashoffset .6s var(--ease-out)" }} />
      </svg>
      <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center" }}>
        <div style={{ textAlign: "center" }}>
          <b style={{ fontFamily: "var(--font-mono)", fontSize: 17 }}>{label != null ? label : p}</b>
          {unit && <span style={{ display: "block", fontFamily: "var(--font-mono)", fontSize: 8.5, color: "var(--ink-3)", letterSpacing: "0.1em" }}>{unit}</span>}
        </div>
      </div>
    </div>
  );
}

/* ----------------------------------------------------------------- GAUGE - */
function Gauge({ p = 64, color = "var(--state-caution)", label, unit, size = 84 }) {
  // 270° arc
  const r = size / 2 - 6, cx = size / 2, cy = size / 2;
  const start = 135, sweep = 270;
  const pol = (a) => [cx + r * Math.cos((a - 90) * Math.PI / 180), cy + r * Math.sin((a - 90) * Math.PI / 180)];
  const arc = (a0, a1) => {
    const [x0, y0] = pol(a0), [x1, y1] = pol(a1);
    const large = a1 - a0 > 180 ? 1 : 0;
    return `M ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1}`;
  };
  return (
    <div className="ring" style={{ width: size, height: size, position: "relative", flex: "none" }}>
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
        <path d={arc(start, start + sweep)} fill="none" stroke="var(--graphite)" strokeWidth="7" strokeLinecap="round" />
        <path d={arc(start, start + sweep * p / 100)} fill="none" stroke={color} strokeWidth="7" strokeLinecap="round" style={{ filter: `drop-shadow(0 0 5px ${color})` }} />
      </svg>
      <div style={{ position: "absolute", inset: 0, display: "grid", placeItems: "center", textAlign: "center" }}>
        <div>
          <b style={{ fontFamily: "var(--font-mono)", fontSize: 16 }}>{label}</b>
          {unit && <span style={{ display: "block", fontFamily: "var(--font-mono)", fontSize: 8.5, color: "var(--ink-3)", letterSpacing: "0.1em" }}>{unit}</span>}
        </div>
      </div>
    </div>
  );
}

/* ------------------------------------------------------------ SPARKLINE - */
function Sparkline({ data, color = "var(--magenta)", h = 56, fill = true, w = 200 }) {
  const max = Math.max(...data), min = Math.min(...data);
  const pts = data.map((v, i) => {
    const x = (i / (data.length - 1)) * w;
    const y = h - 4 - ((v - min) / (max - min || 1)) * (h - 10);
    return [x, y];
  });
  const line = pts.map((p, i) => `${i ? "L" : "M"}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(" ");
  const id = useMemo(() => "spk" + Math.random().toString(36).slice(2, 7), []);
  return (
    <svg width="100%" height={h} viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
      {fill && <defs><linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
        <stop offset="0" stopColor={color} stopOpacity="0.32" /><stop offset="1" stopColor={color} stopOpacity="0" />
      </linearGradient></defs>}
      {fill && <path d={`${line} L${w},${h} L0,${h} Z`} fill={`url(#${id})`} />}
      <path d={line} fill="none" stroke={color} strokeWidth="2" style={{ filter: `drop-shadow(0 0 4px ${color})` }} />
    </svg>
  );
}

/* ----------------------------------------------------------- MICRO BARS - */
function MicroBars({ data, peak, h = 56 }) {
  const max = Math.max(...data);
  return (
    <div style={{ display: "flex", alignItems: "flex-end", gap: 4, height: h }}>
      {data.map((v, i) => {
        const isPeak = i === peak;
        return <i key={i} style={{
          flex: 1, height: `${(v / max) * 100}%`, borderRadius: 2,
          background: isPeak ? "var(--magenta)" : (i % 3 === 0 ? "var(--series-3)" : "var(--series-1)"),
          boxShadow: isPeak ? "0 0 8px var(--magenta)" : "none",
          transformOrigin: "bottom", animation: `barGrow .5s var(--ease-out) ${i * 0.03}s both`,
        }} />;
      })}
    </div>
  );
}

/* ============================================================ EMPIRE MAP = */
/* Canvas dot-matrix world. Continents = unions of ellipses (synthetic geo),
   ~2px violet dots; density encodes activity. Heat nodes pulse; relationship
   arcs flow; ONE radial scan sweep. Colour is truth. Not decoration.        */
const CONTINENTS = [
  // N America
  [0.18,0.30,0.085,0.13],[0.225,0.42,0.06,0.085],[0.135,0.345,0.05,0.075],
  // Greenland
  [0.34,0.17,0.032,0.05],
  // S America
  [0.305,0.66,0.05,0.14],[0.275,0.55,0.038,0.06],
  // Europe
  [0.49,0.31,0.045,0.055],[0.45,0.345,0.03,0.035],
  // Africa
  [0.525,0.56,0.062,0.15],[0.5,0.46,0.04,0.05],
  // Asia
  [0.7,0.32,0.135,0.12],[0.61,0.29,0.06,0.06],[0.78,0.4,0.05,0.05],
  // India
  [0.675,0.46,0.038,0.055],
  // SE Asia / Indonesia
  [0.8,0.55,0.05,0.03],[0.84,0.58,0.03,0.022],
  // Australia
  [0.86,0.73,0.055,0.05],
];
function isLand(x, y) {
  for (const [cx, cy, rx, ry] of CONTINENTS) {
    const dx = (x - cx) / rx, dy = (y - cy) / ry;
    if (dx * dx + dy * dy <= 1) return true;
  }
  return false;
}
function cssVar(name) {
  return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function EmpireMap({ nodes = DW.MAP_NODES, arcs = DW.MAP_ARCS, height = 460, onNode, focusState }) {
  const canvasRef = useRef(null);
  const wrapRef = useRef(null);
  const [hover, setHover] = useState(null);
  const rafRef = useRef(0);

  useEffect(() => {
    const canvas = canvasRef.current, wrap = wrapRef.current;
    const ctx = canvas.getContext("2d");
    let W = 0, H = 0, dots = [], dpr = Math.min(window.devicePixelRatio || 1, 2);
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

    const palette = () => ({
      dot: cssVar("--violet-deep") || "#4F33B8",
      dotLit: cssVar("--violet-ink") || "#CDB6FF",
      states: {
        active: cssVar("--state-active"), crisis: cssVar("--state-crisis"),
        caution: cssVar("--state-caution"), clear: cssVar("--state-clear"),
        focus: cssVar("--state-focus"), standby: cssVar("--state-standby"),
      },
    });
    let pal = palette();

    function build() {
      const rect = wrap.getBoundingClientRect();
      W = rect.width; H = height;
      canvas.width = W * dpr; canvas.height = H * dpr;
      canvas.style.width = W + "px"; canvas.style.height = H + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      dots = [];
      const step = 9;
      for (let py = 14; py < H - 8; py += step) {
        for (let px = 8; px < W - 8; px += step) {
          const nx = px / W, ny = (py - 8) / (H - 22);
          if (isLand(nx, ny)) {
            // density jitter so coastlines feel organic
            if (Math.random() < 0.12) continue;
            dots.push({ x: px + (Math.random() * 2 - 1), y: py + (Math.random() * 2 - 1), nx, ny });
          }
        }
      }
    }

    function nodePx(n) { return { x: n.x * W, y: 8 + n.y * (H - 22) }; }

    function draw(ts) {
      ctx.clearRect(0, 0, W, H);
      const t = ts / 1000;
      // scan sweep angle (single ambient motion)
      const sweep = reduce ? -1 : (t * 0.45) % (Math.PI * 2);
      const cx = W * 0.5, cy = H * 0.46;

      // dots
      for (const d of dots) {
        // proximity to nearest heat node lifts brightness
        let heat = 0, heatColor = null;
        for (const n of nodes) {
          const np = nodePx(n);
          const dist = Math.hypot(np.x - d.x, np.y - d.y);
          if (dist < 70) { const w = 1 - dist / 70; if (w > heat) { heat = w; heatColor = pal.states[n.state]; } }
        }
        // scan sweep highlight
        let beam = 0;
        if (!reduce) {
          const ang = Math.atan2(d.y - cy, d.x - cx);
          let diff = Math.abs(((ang - sweep + Math.PI * 3) % (Math.PI * 2)) - Math.PI);
          diff = Math.PI - diff;
          beam = Math.max(0, 1 - diff * 2.2) * 0.5;
        }
        const base = 0.34 + heat * 0.4 + beam;
        ctx.globalAlpha = Math.min(1, base);
        ctx.fillStyle = heat > 0.35 ? heatColor : pal.dot;
        const r = heat > 0.35 ? 1.7 : 1.3;
        ctx.beginPath(); ctx.arc(d.x, d.y, r, 0, Math.PI * 2); ctx.fill();
      }
      ctx.globalAlpha = 1;

      // arcs (relationships)
      for (const [a, b] of arcs) {
        const na = nodes.find(n => n.id === a), nb = nodes.find(n => n.id === b);
        if (!na || !nb) continue;
        const pa = nodePx(na), pb = nodePx(nb);
        const mx = (pa.x + pb.x) / 2, my = (pa.y + pb.y) / 2 - Math.hypot(pb.x - pa.x, pb.y - pa.y) * 0.28;
        const grad = ctx.createLinearGradient(pa.x, pa.y, pb.x, pb.y);
        const ca = pal.states[na.state], cb = pal.states[nb.state];
        grad.addColorStop(0, ca); grad.addColorStop(1, cb);
        ctx.strokeStyle = grad; ctx.lineWidth = 1; ctx.globalAlpha = 0.28;
        ctx.beginPath(); ctx.moveTo(pa.x, pa.y); ctx.quadraticCurveTo(mx, my, pb.x, pb.y); ctx.stroke();
        // travelling pulse
        if (!reduce) {
          const tp = (t * 0.35 + (a.charCodeAt(0) % 7) / 7) % 1;
          const qx = (1 - tp) * (1 - tp) * pa.x + 2 * (1 - tp) * tp * mx + tp * tp * pb.x;
          const qy = (1 - tp) * (1 - tp) * pa.y + 2 * (1 - tp) * tp * my + tp * tp * pb.y;
          ctx.globalAlpha = 0.9; ctx.fillStyle = cb;
          ctx.beginPath(); ctx.arc(qx, qy, 1.7, 0, Math.PI * 2); ctx.fill();
        }
      }
      ctx.globalAlpha = 1;

      // nodes
      for (const n of nodes) {
        const p = nodePx(n), col = pal.states[n.state];
        const dim = focusState && focusState !== n.state ? 0.25 : 1;
        const pulse = reduce ? 0.6 : 0.5 + 0.5 * Math.abs(Math.sin(t * 1.6 + n.x * 6));
        ctx.globalAlpha = 0.25 * pulse * dim;
        ctx.fillStyle = col;
        ctx.beginPath(); ctx.arc(p.x, p.y, 13 + pulse * 5, 0, Math.PI * 2); ctx.fill();
        ctx.globalAlpha = dim;
        ctx.fillStyle = col;
        ctx.beginPath(); ctx.arc(p.x, p.y, 3.4, 0, Math.PI * 2); ctx.fill();
        ctx.strokeStyle = "rgba(6,6,12,0.9)"; ctx.lineWidth = 1.4; ctx.stroke();
        if (hover === n.id) {
          ctx.globalAlpha = 0.9; ctx.strokeStyle = col; ctx.lineWidth = 1;
          ctx.beginPath(); ctx.arc(p.x, p.y, 9, 0, Math.PI * 2); ctx.stroke();
        }
      }
      ctx.globalAlpha = 1;
    }

    function onMove(e) {
      const rect = canvas.getBoundingClientRect();
      const mx = e.clientX - rect.left, my = e.clientY - rect.top;
      let found = null;
      for (const n of nodes) { const p = nodePx(n); if (Math.hypot(p.x - mx, p.y - my) < 14) { found = n.id; break; } }
      setHover(found);
      canvas.style.cursor = found ? "pointer" : "default";
    }
    function onClick() { if (hover && onNode) { const n = nodes.find(x => x.id === hover); onNode(n); } }

    build();
    draw(performance.now());           // guaranteed first paint (rAF may be throttled in some embeds)
    // Drive the loop with setInterval so it paints even where rAF is paused.
    // ~33ms ≈ 30fps; ambient motion only, cheap.
    const start = performance.now();
    const tick = () => { if (!document.body.classList.contains("no-motion")) draw(performance.now()); };
    const loopInt = setInterval(tick, 33);
    const ro = new ResizeObserver(() => { pal = palette(); build(); draw(performance.now()); });
    ro.observe(wrap);
    const themeInt = setInterval(() => { pal = palette(); }, 1000); // pick up tweak/state recolours
    canvas.addEventListener("mousemove", onMove);
    canvas.addEventListener("click", onClick);
    canvas.addEventListener("mouseleave", () => setHover(null));
    return () => {
      cancelAnimationFrame(rafRef.current); clearInterval(loopInt); ro.disconnect(); clearInterval(themeInt);
      canvas.removeEventListener("mousemove", onMove);
      canvas.removeEventListener("click", onClick);
    };
  }, [height, nodes, arcs, hover, focusState, onNode]);

  const hn = hover ? nodes.find(n => n.id === hover) : null;
  return (
    <div ref={wrapRef} style={{ position: "relative", width: "100%", height }}>
      <canvas ref={canvasRef} style={{ display: "block" }} />
      {/* graticule sweep ring overlay */}
      <div style={{ position: "absolute", inset: 0, pointerEvents: "none", borderRadius: "inherit",
        background: "radial-gradient(60% 60% at 50% 46%, transparent 60%, rgba(6,6,12,0.35))" }} />
      {hn && (() => {
        const meta = DW.STATE_META[hn.state];
        const ag = hn.agent ? DW.agent(hn.agent) : null;
        return <div style={{
          position: "absolute", left: `min(${hn.x * 100}%, calc(100% - 200px))`, top: `calc(${8 + hn.y * (height - 22)}px - 118px)`,
          pointerEvents: "none", background: "var(--panel-3)", border: "1px solid var(--hairline-3)",
          borderRadius: 8, padding: "9px 11px", boxShadow: "var(--shadow-3)", minWidth: 178, zIndex: 5,
        }}>
          {ag && (
            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 8, paddingBottom: 8, borderBottom: "1px solid var(--hairline)" }}>
              <img src={ag.head} alt={ag.name} style={{ width: 30, height: 30, borderRadius: 9, objectFit: "cover", flex: "none", border: `1px solid color-mix(in srgb, ${ag.ac} 55%, transparent)`, boxShadow: `0 0 10px color-mix(in srgb, ${ag.ac} 35%, transparent)` }} />
              <div style={{ minWidth: 0 }}>
                <div style={{ fontSize: 12.5, fontFamily: "var(--font-display)", fontWeight: 600, lineHeight: 1.1 }}>{ag.name}</div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--ink-3)", textTransform: "uppercase", letterSpacing: "0.08em", marginTop: 2 }}>{ag.domain} · owner</div>
              </div>
            </div>
          )}
          <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
            <span style={{ width: 8, height: 8, borderRadius: 99, background: meta.c, boxShadow: `0 0 8px ${meta.c}` }} />
            <b style={{ fontSize: 13, fontFamily: "var(--font-display)" }}>{hn.label}</b>
          </div>
          <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--ink-3)", marginTop: 4, textTransform: "uppercase", letterSpacing: "0.08em" }}>{hn.kind} · {meta.label}</div>
          <div style={{ fontSize: 12, color: "var(--ink-2)", marginTop: 4 }}>{hn.v}</div>
        </div>;
      })()}
    </div>
  );
}

Object.assign(window, { Icon, StateChip, Led, Ring, Gauge, Sparkline, MicroBars, EmpireMap, cssVar });
