/* components.jsx — shared atoms for KASAU. Exports to window. */
const { useState, useEffect, useRef } = React;

/* Halftone / duotone placeholder with HUD readout */
function Placeholder({ label = "image", variant = "", className = "", style = {}, ratio, id = "IMG", tag }) {
  const s = { ...style };
  if (ratio) s.aspectRatio = ratio;
  return (
    <div className={`ph ${variant} ${className}`} style={s}>
      <span className="tick tl"></span>
      <span className="tick tr"></span>
      <span className="tick bl"></span>
      <span className="tick br"></span>
      <span className="ph-id">{id}</span>
      <span className="ph-cap">{label}</span>
      {tag && <span className="art-tag">{tag}</span>}
    </div>
  );
}

/* Arrow glyph */
function Arrow() { return <span className="arrow">→</span>; }

/* Sticker / chevron HUD atoms */
function Sticker({ children, acc }) {
  return <span className={"sticker" + (acc ? " acc" : "")}><span className="sq"></span>{children}</span>;
}

/* Button that can navigate or act */
function Btn({ children, to, onClick, variant = "", size = "", arrow = false, type, disabled = false }) {
  const cls = `btn ${variant === "ghost" ? "btn-ghost" : ""} ${size === "lg" ? "btn-lg" : ""}`;
  const handle = (e) => {
    if (onClick) onClick(e);
    if (to) { e.preventDefault(); window.__navigate(to); }
  };
  if (type === "submit") {
    return <button type="submit" className={cls} onClick={onClick} disabled={disabled}>{children}{arrow && <Arrow />}</button>;
  }
  return (
    <a href={to || "#"} className={cls} onClick={handle}>
      {children}{arrow && <Arrow />}
    </a>
  );
}

/* Kicker with bar */
function Kicker({ children, center }) {
  return (
    <div className="hero-kicker" style={center ? { justifyContent: "center" } : null}>
      <span className="bar"></span>
      <span className="kicker">{children}</span>
    </div>
  );
}

/* Scroll-reveal wrapper */
function Reveal({ children, delay = 0, as = "div", className = "", ...rest }) {
  const ref = useRef(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const vh = window.innerHeight || 800;
    const reveal = () => requestAnimationFrame(() => el && el.classList.add("in"));
    // already at/near the fold on mount → reveal now
    const r = el.getBoundingClientRect();
    if (r.top < vh * 1.15) { reveal(); return; }
    const io = new IntersectionObserver((entries) => {
      entries.forEach((en) => {
        if (en.isIntersecting) { setTimeout(reveal, delay); io.unobserve(el); }
      });
    }, { threshold: 0, rootMargin: "0px 0px 12% 0px" });
    io.observe(el);
    // safety net: never let content stay hidden
    const t = setTimeout(() => { if (el && !el.classList.contains("in")) {
      const rr = el.getBoundingClientRect();
      if (rr.top < (window.innerHeight || 800)) reveal();
    } }, 1400);
    return () => { io.disconnect(); clearTimeout(t); };
  }, [delay]);
  const Tag = as;
  return <Tag ref={ref} className={`reveal ${className}`} {...rest}>{children}</Tag>;
}

/* Domain ticker */
function Ticker({ items }) {
  const trackRef = useRef(null);
  const setRef = useRef(null);
  const [cloneN, setCloneN] = useState(Math.min(items.length, 6));

  useEffect(() => {
    const track = trackRef.current, set = setRef.current;
    if (!track || !set) return;
    let anim = null;
    const build = () => {
      const w = set.scrollWidth;
      if (!w) return;
      // how many leading items are needed to cover the viewport at the loop seam
      const kids = [...set.children];
      let acc = 0, need = 1;
      for (let i = 0; i < kids.length; i++) {
        acc += kids[i].getBoundingClientRect().width;
        need = i + 1;
        if (acc >= window.innerWidth + 48) break;
      }
      if (need !== cloneN) { setCloneN(need); return; } // re-render, effect re-runs
      if (anim) anim.cancel();
      // pixel-precise transform → stays on the compositor; layer = one list + viewport
      anim = track.animate(
        [{ transform: "translateX(0px)" }, { transform: "translateX(" + (-w) + "px)" }],
        { duration: (w / 72.5) * 1000, iterations: Infinity, easing: "linear" }
      );
    };
    build();
    const ro = new ResizeObserver(build);
    ro.observe(set);
    window.addEventListener("resize", build);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(build);
    const pause = () => anim && anim.pause();
    const play = () => anim && anim.play();
    track.addEventListener("mouseenter", pause);
    track.addEventListener("mouseleave", play);
    return () => {
      ro.disconnect();
      window.removeEventListener("resize", build);
      track.removeEventListener("mouseenter", pause);
      track.removeEventListener("mouseleave", play);
      if (anim) anim.cancel();
    };
  }, [items, cloneN]);

  const lead = items.slice(0, cloneN);
  return (
    <div className="ticker">
      <div className="ticker-track" ref={trackRef}>
        <div className="ticker-set" ref={setRef}>
          {items.map((it, i) => (
            <span className="ticker-item" key={"a" + i}>{it}<span className="sep">/</span></span>
          ))}
        </div>
        <div className="ticker-set" aria-hidden="true">
          {lead.map((it, i) => (
            <span className="ticker-item" key={"b" + i}>{it}<span className="sep">/</span></span>
          ))}
        </div>
      </div>
    </div>
  );
}

/* HUD agent-topology graphic — replaces image placeholders */
function HudGraphic({ id = "SYS·01", tag = "LIVE // KASAU", style = {} }) {
  const hub = { x: 60, y: 50 };
  const nodes = [
    { x: 22, y: 21, l: "AGENT" },
    { x: 99, y: 27, l: "MODEL" },
    { x: 15, y: 63, l: "PREDICT" },
    { x: 93, y: 73, l: "GUARD" },
    { x: 56, y: 13, l: "" },
    { x: 41, y: 86, l: "FINE-TUNE" },
    { x: 107, y: 52, l: "" },
    { x: 73, y: 90, l: "" },
  ];
  const cross = [[0, 2], [1, 6], [3, 7], [5, 2], [4, 1]];
  return (
    <div className="ph hud" style={style}>
      <span className="tick tl"></span><span className="tick tr"></span>
      <span className="tick bl"></span><span className="tick br"></span>
      <span className="ph-id">{id}</span>
      <svg className="hud-svg" viewBox="0 0 120 100" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
        <defs>
          <linearGradient id="kxSweep" x1="0" y1="0" x2="1" y2="0">
            <stop className="s0" offset="0%"></stop>
            <stop className="s1" offset="100%"></stop>
          </linearGradient>
        </defs>
        <g className="rings">
          <circle cx="60" cy="50" r="16"></circle>
          <circle cx="60" cy="50" r="30"></circle>
          <circle cx="60" cy="50" r="44"></circle>
        </g>
        <g className="sweep-g">
          <path className="sweep" d="M60 50 L60 2 L104 14 Z" fill="url(#kxSweep)">
            <animateTransform attributeName="transform" type="rotate" from="0 60 50" to="360 60 50" dur="9s" repeatCount="indefinite"></animateTransform>
          </path>
        </g>
        {cross.map(([a, b], i) => (
          <line key={"c" + i} className="edge dim" x1={nodes[a].x} y1={nodes[a].y} x2={nodes[b].x} y2={nodes[b].y}></line>
        ))}
        {nodes.map((n, i) => (
          <line key={"e" + i} className="edge" x1={hub.x} y1={hub.y} x2={n.x} y2={n.y}></line>
        ))}
        {nodes.map((n, i) => (
          <line key={"f" + i} className="flow" x1={hub.x} y1={hub.y} x2={n.x} y2={n.y} style={{ animationDelay: (i * 0.3) + "s" }}></line>
        ))}
        {nodes.map((n, i) => (
          <circle key={"n" + i} className="node" cx={n.x} cy={n.y} r="2"></circle>
        ))}
        <circle className="hub-ring" cx="60" cy="50" r="6"></circle>
        <circle className="hub" cx="60" cy="50" r="3.4"></circle>
        <line className="xh" x1="51" y1="50" x2="69" y2="50"></line>
        <line className="xh" x1="60" y1="41" x2="60" y2="59"></line>
        {nodes.map((n, i) => (n.l ?
          <text key={"t" + i} className="nlabel" x={n.x} y={n.y - 3.6} textAnchor="middle">{n.l}</text> : null
        ))}
      </svg>
      <span className="art-tag">{tag}</span>
    </div>
  );
}

/* ============================================================
   SITE GRAPH — one agent-topology spine threading the whole page.
   Every [data-node] section is a node; scroll traverses the network.
   ============================================================ */
function SiteGraph({ trigger }) {
  const [lay, setLay] = useState(null);   // { W, docH, spineX, padL, nodes }
  const [phY, setPhY] = useState(0);      // playhead Y (px)
  const [act, setAct] = useState(0);      // active node index
  const raf = useRef(0);

  useEffect(() => {
    let on = true;
    const anchorY = (el) => Math.round(el.getBoundingClientRect().top + window.scrollY + 34);

    const measure = () => {
      const els = [...document.querySelectorAll("[data-node]")];
      const W = window.innerWidth;
      if (!on) return;
      if (els.length < 2 || W < 900) { setLay(null); return; }
      const wrap = document.querySelector(".wrap");
      const padL = wrap ? parseFloat(getComputedStyle(wrap).paddingLeft) : 60;
      const spineX = Math.max(20, Math.min(padL * 0.46, 42));
      // Measure true content height from the footer (normal flow). Using
      // scrollHeight here would include this absolutely-positioned SVG itself,
      // which locks the height tall and can't shrink on shorter inner pages.
      const footer = document.querySelector(".site-footer");
      const contentBottom = footer
        ? Math.round(footer.getBoundingClientRect().bottom + window.scrollY)
        : Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
      const docH = Math.max(contentBottom, window.innerHeight);
      const nodes = els.map((el, i) => ({
        y: anchorY(el),
        label: el.getAttribute("data-node") || "",
        num: String(i).padStart(2, "0"),
      }));
      setLay({ W, docH, spineX, padL, nodes });
    };

    const tick = () => {
      cancelAnimationFrame(raf.current);
      raf.current = requestAnimationFrame(() => {
        const els = [...document.querySelectorAll("[data-node]")];
        if (els.length < 2 || window.innerWidth < 900) return;
        const ys = els.map(anchorY);
        const top = ys[0], bot = ys[ys.length - 1];
        const focus = window.scrollY + window.innerHeight * 0.42;
        setPhY(Math.max(top, Math.min(bot, focus)));
        let best = 0, bd = Infinity;
        ys.forEach((y, i) => { const d = Math.abs(y - focus); if (d < bd) { bd = d; best = i; } });
        setAct(best);
      });
    };

    const run = () => { measure(); tick(); };
    run();
    const ro = new ResizeObserver(run);
    ro.observe(document.body);
    window.addEventListener("scroll", tick, { passive: true });
    window.addEventListener("resize", run);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(run);
    const t1 = setTimeout(run, 340);
    const t2 = setTimeout(run, 950);
    return () => {
      on = false; ro.disconnect();
      window.removeEventListener("scroll", tick);
      window.removeEventListener("resize", run);
      cancelAnimationFrame(raf.current); clearTimeout(t1); clearTimeout(t2);
    };
  }, [trigger]);

  if (!lay) return null;
  const { W, docH, spineX, padL, nodes } = lay;
  const top = nodes[0].y, bot = nodes[nodes.length - 1].y;
  const tickLen = Math.max(8, Math.min(padL - spineX - 12, 26));

  const jump = (i) => {
    const el = document.querySelectorAll("[data-node]")[i];
    if (!el) return;
    const y = el.getBoundingClientRect().top + window.scrollY - 100;
    window.scrollTo({ top: Math.max(0, y), behavior: "smooth" });
  };

  return (
    <svg className="sitegraph" width={W} height={docH} aria-hidden="true">
      <line className="sg-spine" x1={spineX} y1={top} x2={spineX} y2={bot} />
      {nodes.slice(0, -1).map((n, i) => {
        const my = (nodes[i].y + nodes[i + 1].y) / 2;
        return <line key={"b" + i} className="sg-branch"
          x1={spineX} y1={my} x2={spineX + (i % 2 ? 16 : -16)} y2={my + 18} />;
      })}
      <line className="sg-live" x1={spineX} y1={top} x2={spineX} y2={phY} />
      <line className="sg-flow" x1={spineX} y1={top} x2={spineX} y2={phY} />
      {nodes.map((n, i) => {
        const isOn = i === act;
        const passed = n.y <= phY + 4;
        const cls = "sg-ng" + (isOn ? " on" : "") + (passed ? " passed" : "");
        return (
          <g key={"n" + i} className={cls}>
            <line className="sg-tick" x1={spineX} y1={n.y} x2={spineX + tickLen} y2={n.y} />
            <text className="sg-num" x={spineX + tickLen + 5} y={n.y + 3}>{n.num}</text>
            <text className="sg-label" textAnchor="start"
              transform={`rotate(-90 ${spineX - 13} ${n.y})`} x={spineX - 13} y={n.y}>{n.label}</text>
            <circle className="sg-dot" cx={spineX} cy={n.y} r={isOn ? 5 : 3.4} />
            {isOn && (
              <circle className="sg-ring" cx={spineX} cy={n.y} r="5">
                <animate attributeName="r" from="5" to="15" dur="1.8s" repeatCount="indefinite" />
                <animate attributeName="opacity" from="0.7" to="0" dur="1.8s" repeatCount="indefinite" />
              </circle>
            )}
            <circle className="sg-hit" cx={spineX} cy={n.y} r="16" onClick={() => jump(i)} />
          </g>
        );
      })}
      <circle className="sg-ph-glow" cx={spineX} cy={phY} r="7" />
      <circle className="sg-ph" cx={spineX} cy={phY} r="3" />
    </svg>
  );
}

/* ============================================================
   RIGHT FIELD — full-page agent network living behind the whole site.
   Three generated modes: constellation / mesh / streams.
   ============================================================ */
function rf_rng(seed) { let s = seed >>> 0; return () => { s = (s * 1664525 + 1013904223) >>> 0; return s / 4294967296; }; }

function buildField(mode) {
  if (mode === "streams") {
    const R = rf_rng(19);
    const xs = [42, 50, 58, 66, 74, 82, 90, 97];
    const cols = xs.map((x) => {
      const nodes = []; let y = 2 + R() * 8;
      while (y < 98) { nodes.push(+y.toFixed(2)); y += 9 + R() * 16; }
      return { x, nodes };
    });
    const links = [];
    for (let i = 0; i < 8; i++) {
      const ci = Math.floor(R() * (xs.length - 1));
      links.push({ x1: xs[ci], x2: xs[ci + 1], y: +(8 + R() * 84).toFixed(2) });
    }
    return { mode, cols, links };
  }
  if (mode === "mesh") {
    const R = rf_rng(7);
    const N = 46, nodes = [];
    for (let i = 0; i < N; i++) nodes.push({ x: +(34 + R() * 66).toFixed(2), y: +(2 + R() * 96).toFixed(2), r: 0.3 + R() * 0.5 });
    const edges = [], seen = new Set();
    nodes.forEach((n, i) => {
      const d = nodes.map((m, j) => ({ j, d: (m.x - n.x) ** 2 + (m.y - n.y) ** 2 })).filter((o) => o.j !== i).sort((a, b) => a.d - b.d);
      const k = 2 + (R() > 0.6 ? 1 : 0);
      for (let t = 0; t < k && t < d.length; t++) {
        const j = d[t].j, key = i < j ? i + "-" + j : j + "-" + i;
        if (!seen.has(key)) { seen.add(key); edges.push([i, j]); }
      }
    });
    const flows = [];
    for (let i = 0; i < 12; i++) flows.push(edges[Math.floor(R() * edges.length)]);
    return { mode, nodes, edges, flows };
  }
  // constellation
  const R = rf_rng(3);
  const center = { x: 98, y: 50 };
  const rings = [14, 24, 36, 50, 66, 82];
  const N = 30, nodes = [];
  for (let i = 0; i < N; i++) {
    const a = (100 + R() * 160) * Math.PI / 180, rad = 10 + R() * 72;
    let x = center.x + Math.cos(a) * rad, y = center.y + Math.sin(a) * rad;
    x = Math.max(26, Math.min(100, x)); y = Math.max(2, Math.min(98, y));
    nodes.push({ x: +x.toFixed(2), y: +y.toFixed(2), r: 0.3 + R() * 0.5, a });
  }
  const sorted = nodes.map((n, i) => ({ i, a: n.a })).sort((p, q) => p.a - q.a);
  const cross = [];
  for (let t = 0; t < sorted.length - 1; t++) if (R() > 0.45) cross.push([sorted[t].i, sorted[t + 1].i]);
  const flows = [];
  for (let i = 0; i < 10; i++) flows.push(Math.floor(R() * nodes.length));
  return { mode, center, rings, nodes, cross, flows };
}

function RightField({ mode = "constellation" }) {
  const f = React.useMemo(() => buildField(mode), [mode]);
  if (mode === "off") return null;
  return (
    <div className={"right-field rf-" + mode} aria-hidden="true">
      <svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice">
        <defs>
          <radialGradient id="rfSweep" cx="100%" cy="50%" r="80%">
            <stop offset="0%" stopColor="currentColor" stopOpacity="0.16"></stop>
            <stop offset="100%" stopColor="currentColor" stopOpacity="0"></stop>
          </radialGradient>
        </defs>

        {mode === "constellation" && (
          <g>
            <animateTransform attributeName="transform" type="translate" values="0 0; -1.4 -1; 0 0" dur="26s" repeatCount="indefinite"></animateTransform>
            {f.rings.map((r, i) => <circle key={"r" + i} className="rf-ring" cx={f.center.x} cy={f.center.y} r={r}></circle>)}
            <g className="rf-sweep-g">
              <path className="rf-sweep" d={`M${f.center.x} ${f.center.y} L${f.center.x - 82} ${f.center.y - 30} A82 82 0 0 1 ${f.center.x - 82} ${f.center.y + 30} Z`}></path>
              <animateTransform attributeName="transform" type="rotate" from={`0 ${f.center.x} ${f.center.y}`} to={`360 ${f.center.x} ${f.center.y}`} dur="24s" repeatCount="indefinite"></animateTransform>
            </g>
            {f.nodes.map((n, i) => <line key={"e" + i} className="rf-edge" x1={f.center.x} y1={f.center.y} x2={n.x} y2={n.y}></line>)}
            {f.cross.map(([a, b], i) => <line key={"c" + i} className="rf-edge" x1={f.nodes[a].x} y1={f.nodes[a].y} x2={f.nodes[b].x} y2={f.nodes[b].y}></line>)}
            {f.flows.map((ni, i) => <line key={"fl" + i} className="rf-flow" x1={f.center.x} y1={f.center.y} x2={f.nodes[ni].x} y2={f.nodes[ni].y} style={{ animationDelay: (i * 0.4) + "s" }}></line>)}
            {f.nodes.map((n, i) => <circle key={"n" + i} className="rf-node" cx={n.x} cy={n.y} r={n.r} style={{ animationDelay: (i * 0.18) + "s" }}></circle>)}
            <circle className="rf-hub" cx={f.center.x} cy={f.center.y} r="1.6"></circle>
          </g>
        )}

        {mode === "mesh" && (
          <g>
            <animateTransform attributeName="transform" type="translate" values="0 0; -1.6 1.2; 0 0" dur="30s" repeatCount="indefinite"></animateTransform>
            {f.edges.map(([a, b], i) => <line key={"e" + i} className="rf-edge" x1={f.nodes[a].x} y1={f.nodes[a].y} x2={f.nodes[b].x} y2={f.nodes[b].y}></line>)}
            {f.flows.map((e, i) => e ? <line key={"fl" + i} className="rf-flow" x1={f.nodes[e[0]].x} y1={f.nodes[e[0]].y} x2={f.nodes[e[1]].x} y2={f.nodes[e[1]].y} style={{ animationDelay: (i * 0.35) + "s" }}></line> : null)}
            {f.nodes.map((n, i) => <circle key={"n" + i} className="rf-node" cx={n.x} cy={n.y} r={n.r} style={{ animationDelay: (i * 0.14) + "s" }}></circle>)}
          </g>
        )}

        {mode === "streams" && (
          <g>
            {f.links.map((l, i) => <line key={"l" + i} className="rf-edge" x1={l.x1} y1={l.y} x2={l.x2} y2={l.y}></line>)}
            {f.cols.map((c, ci) => (
              <g key={"col" + ci}>
                <line className="rf-stream" x1={c.x} y1="0" x2={c.x} y2="100" style={{ animationDelay: (ci * 0.3) + "s" }}></line>
                {c.nodes.map((y, ni) => <circle key={ni} className="rf-node" cx={c.x} cy={y} r="0.55" style={{ animationDelay: ((ci + ni) * 0.2) + "s" }}></circle>)}
              </g>
            ))}
          </g>
        )}
      </svg>
    </div>
  );
}

/* Bold labeled agent network — the focal graphic in every hero */
function HeroNet({ id = "SYS·01" }) {
  const hub = { x: 75, y: 62 };
  const nodes = [
    { x: 75,  y: 24,  l: "AGENT",     a: "middle", dx: 0,  dy: -5 },
    { x: 120, y: 50,  l: "MODEL",     a: "end",    dx: -6, dy: -4 },
    { x: 82,  y: 102, l: "PREDICT",   a: "middle", dx: 0,  dy: 9  },
    { x: 30,  y: 58,  l: "GUARD",     a: "start",  dx: 6,  dy: -4 },
    { x: 110, y: 18,  l: "SAFETY",    a: "end",    dx: -5, dy: -3 },
    { x: 128, y: 92,  l: "FORECAST",  a: "end",    dx: -6, dy: 8  },
    { x: 48,  y: 106, l: "FINE-TUNE", a: "middle", dx: 0,  dy: 9  },
    { x: 16,  y: 96,  l: "",          a: "start" },
    { x: 18,  y: 28,  l: "LLM API",   a: "start",  dx: 6,  dy: -3 },
    { x: 56,  y: 10,  l: "",          a: "middle" },
    { x: 138, y: 64,  l: "",          a: "end" },
    { x: 96,  y: 118, l: "",          a: "middle" },
  ];
  const spokes = [0, 1, 2, 3, 4, 5, 6, 8];
  const cross = [[0,4],[4,1],[1,5],[5,2],[2,6],[6,7],[7,3],[3,8],[8,9],[9,0],[1,10],[5,10],[6,11],[2,11]];
  const flows = [0, 1, 2, 3, 4, 5, 6, 8];
  return (
    <svg className="heronet" viewBox="0 0 150 124" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
      <defs>
        <radialGradient id={"hnS" + id} cx="50%" cy="50%" r="50%">
          <stop offset="0%" stopColor="currentColor" stopOpacity="0.22"></stop>
          <stop offset="100%" stopColor="currentColor" stopOpacity="0"></stop>
        </radialGradient>
      </defs>
      <g className="hn-rings">
        {[20, 34, 50, 66].map((r, i) => <circle key={i} className="hn-ring" cx={hub.x} cy={hub.y} r={r}></circle>)}
      </g>
      <g className="hn-sweep-g">
        <path className="hn-sweep" style={{ fill: `url(#hnS${id})` }} d={`M${hub.x} ${hub.y} L${hub.x-66} ${hub.y-26} A71 71 0 0 1 ${hub.x-66} ${hub.y+26} Z`}></path>
        <animateTransform attributeName="transform" type="rotate" from={`0 ${hub.x} ${hub.y}`} to={`360 ${hub.x} ${hub.y}`} dur="16s" repeatCount="indefinite"></animateTransform>
      </g>
      {cross.map(([a, b], i) => (
        <line key={"c" + i} className="hn-edge dim" x1={nodes[a].x} y1={nodes[a].y} x2={nodes[b].x} y2={nodes[b].y}></line>
      ))}
      {spokes.map((ni, i) => (
        <line key={"s" + i} className="hn-edge" x1={hub.x} y1={hub.y} x2={nodes[ni].x} y2={nodes[ni].y}></line>
      ))}
      {flows.map((ni, i) => (
        <line key={"f" + i} className="hn-flow" x1={hub.x} y1={hub.y} x2={nodes[ni].x} y2={nodes[ni].y} style={{ animationDelay: (i * -0.32) + "s" }}></line>
      ))}
      {nodes.map((n, i) => (
        <circle key={"n" + i} className={"hn-node" + (n.l ? " lit" : "")} cx={n.x} cy={n.y} r={n.l ? 2.4 : 1.5}></circle>
      ))}
      <circle className="hn-hubring" cx={hub.x} cy={hub.y} r="6"></circle>
      <circle className="hn-hub" cx={hub.x} cy={hub.y} r="3.6"></circle>
      <line className="hn-xh" x1={hub.x - 3.4} y1={hub.y} x2={hub.x + 3.4} y2={hub.y}></line>
      <line className="hn-xh" x1={hub.x} y1={hub.y - 3.4} x2={hub.x} y2={hub.y + 3.4}></line>
      {nodes.map((n, i) => (n.l ?
        <text key={"t" + i} className="hn-label" x={n.x + (n.dx || 0)} y={n.y + (n.dy || 0)} textAnchor={n.a}>{n.l}</text> : null
      ))}
    </svg>
  );
}

Object.assign(window, { Placeholder, Arrow, Btn, Kicker, Reveal, Ticker, Sticker, HudGraphic, SiteGraph, RightField, HeroNet });
