    import ForceGraph2D from 'https://esm.sh/react-force-graph-2d?external=react';
    import React from 'react';
    import { createRoot } from 'react-dom/client';
    import { forceCollide, forceRadial } from 'https://esm.sh/d3-force';
    import { NavBar, SearchBox, ThemeToggle, DetailHeader, useTheme, setupLayout, useVizPaneSize, useGraphPhysics, RENDER, PALETTE, normalizeNodeWeights, drawLabel, linkEnds, useGraphState, useBraveClickFix, CornerPanel, nodePointerAreaPaint, paintNodeColors, FONT_MONO } from '/shell.js';

    setupLayout();

    // ─── constants ───────────────────────────────────────────────────────────

    const COLORS = {
      rule:     PALETTE.ENTRY,
      decision: PALETTE.LEAF,
    };

    const NODE_TYPES = ['rule', 'decision'];

    const TYPE_LABELS = {
      rule:     'platform-rules',
      decision: 'knowledge graph',
    };

    const dotRadius = node =>
      node.type === 'decision' ? 2 + node.__weight * 7 : 5;

    // ─── data prep ───────────────────────────────────────────────────────────

    const raw = await fetch('/knowledge-graph.json').then(r => r.json());

    raw.nodes = raw.nodes.filter(n => n.type !== 'unknown');
    const ids = new Set(raw.nodes.map(n => n.id));
    raw.links = raw.links.filter(l => ids.has(l.source) && ids.has(l.target));

    normalizeNodeWeights(raw);

    const nodeMap = new Map(raw.nodes.map(n => [n.id, n]));
    raw.links.forEach(l => {
      if (l.relationship === 'related' && !l.note) {
        const src = nodeMap.get(l.source);
        const entry = src?.links?.find(e => e.id === l.target);
        if (entry?.note) l.note = entry.note;
      }
    });

    const rim = raw.nodes.filter(n => n.type === 'rule');
    rim.forEach((n, i) => {
      const angle = (2 * Math.PI * i) / rim.length - Math.PI / 4;
      n.x = Math.cos(angle) * 105;
      n.y = Math.sin(angle) * 105;
    });

    raw.nodes
      .filter(n => n.type !== 'rule')
      .sort((a, b) => b.__weight - a.__weight)
      .slice(0, 1)
      .forEach(n => { n.fx = 0; n.fy = 0; });

    // ─── hero panel ──────────────────────────────────────────────────────────

    function HeroPanel({ theme }) {
      return (
        <div className={"corner-panel" + (theme === 'light' ? ' light' : '')}>
          <h1 style={{ margin: '0 0 12px', padding: 0, fontSize: 16, fontWeight: 400, lineHeight: 1.3, color: '#fff' }}>
            Gazelle — Engine for Azure Landing Zones
          </h1>
          <p style={{ margin: '0 0 14px', fontSize: 14, color: 'rgba(155,175,215,0.75)', letterSpacing: '0.03em' }}>
            Explore the platform knowledge graph
          </p>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
            <p className="info-text" style={{ margin: 0 }}>This graph traces the reasoning behind Gazelle's design — how each design decision works together to give teams autonomy through self-service.</p>
            <hr style={{ border: 'none', borderTop: '1px solid rgba(155,175,215,0.15)', margin: '4px 0' }} />
            <p className="info-text" style={{ margin: 0 }}><span style={{ color: '#f0a500' }}>Yellow</span> = guiding principles</p>
            <p className="info-text" style={{ margin: 0 }}><span style={{ color: '#4e8ef7' }}>Blue</span> = design decisions</p>
            <hr style={{ border: 'none', borderTop: '1px solid rgba(155,175,215,0.15)', margin: '4px 0' }} />
            <p className="info-text" style={{ margin: 0 }}>
              This entire platform, code and knowledge graph included, is on <a href="https://github.com/orgs/gazelle-cloud/" target="_blank" rel="noopener noreferrer" style={{ color: 'rgba(155,175,215,0.85)', textDecoration: 'none', borderBottom: '1px solid rgba(155,175,215,0.3)' }}>GitHub</a>.
            </p>
          </div>
        </div>
      );
    }

    // ─── component ───────────────────────────────────────────────────────────

    function Graph() {
      const fgRef = React.useRef();
      const { theme, toggleTheme, bgColor } = useTheme();

      const size = useVizPaneSize(fgRef);

      const { activeId, graph, activeNode, neighbours, setHoveredId, setFocusedId, onNodeClick, onBackgroundClick, searchQuery, setSearchQuery, searchMatchIds } = useGraphState(raw, NODE_TYPES);
      const handleBackgroundClick = useBraveClickFix(fgRef, graph.nodes, dotRadius, setFocusedId, onBackgroundClick);

      const [searchFocused, setSearchFocused] = React.useState(false);

      // Auto-focus node from URL hash (e.g. /knowledge-graph/#deny-by-default)
      React.useEffect(() => {
        const hash = window.location.hash.slice(1);
        if (hash && graph.nodes.some(n => n.id === hash)) {
          setFocusedId(hash);
        }
      }, []);

      const { outLinks, inLinks } = React.useMemo(() => {
        if (!activeId) return { outLinks: [], inLinks: [] };
        return {
          outLinks: graph.links.filter(l => {
            const [s] = linkEnds(l);
            return s === activeId && l.relationship === 'related';
          }),
          inLinks: graph.links.filter(l => {
            const [, t] = linkEnds(l);
            return t === activeId && l.relationship === 'related';
          }),
        };
      }, [activeId, graph.links]);

      const physicsProps = useGraphPhysics(fgRef, graph, fg => {
        fg.d3Force('link').distance(60).strength(0.05);
        fg.d3Force('collision', forceCollide(n => dotRadius(n) + 3));
        fg.d3Force('charge').strength(n => n.type === 'rule' ? 0 : -5);
        fg.d3Force('radial', forceRadial(n => n.type === 'rule' ? 105 : (1 - n.__weight) * 110, 0, 0)
          .strength(n => n.type === 'rule' ? 0.8 : 1.0));
      });

      const OUTBOUND_COLOR = '#fb923c';
      const INBOUND_COLOR  = '#7dd3fc';

      const linkColor = React.useCallback(l => {
        const dim    = theme === 'dark' ? 'rgba(180,195,225,0.28)' : 'rgba(80,80,100,0.15)';
        const active = theme === 'dark' ? 'rgba(235,242,255,0.95)' : 'rgba(30,30,50,0.75)';
        const faded  = theme === 'dark' ? 'rgba(180,180,180,0.04)' : 'rgba(80,80,100,0.04)';
        if (!activeId) return dim;
        const [s, t] = linkEnds(l);
        if (l.relationship === 'related') {
          if (s === activeId) return OUTBOUND_COLOR;
          if (t === activeId) return INBOUND_COLOR;
        }
        return (s === activeId || t === activeId) ? active : faded;
      }, [activeId, theme]);

      const paintNode = React.useCallback((node, ctx, globalScale) => {
        const isActive    = node.id === activeId;
        const isNeighbour = neighbours?.has(node.id);
        const isMatch     = searchMatchIds.has(node.id);
        const dimmed      = activeId
          ? !isActive && !isNeighbour
          : searchMatchIds.size > 0 && !isMatch;
        const r           = dotRadius(node) / globalScale;

        const { activeColor, backdrop } = paintNodeColors(theme);

        ctx.globalAlpha = dimmed ? RENDER.dimmedAlpha : 1;
        ctx.beginPath();
        ctx.arc(node.x, node.y, r, 0, 2 * Math.PI);
        ctx.fillStyle = isActive ? activeColor : COLORS[node.type];
        ctx.fill();

        const isRim     = node.type === 'rule';
        const showLabel = isRim || isActive || isNeighbour || isMatch;
        if (showLabel) {
          const gap = r + RENDER.labelGap / globalScale;
          let lx, ly, align;
          if (isRim) {
            const angle = Math.atan2(node.y, node.x);
            lx    = node.x + Math.cos(angle) * gap;
            ly    = node.y + Math.sin(angle) * gap;
            align = Math.cos(angle) >= 0 ? 'left' : 'right';
          } else {
            lx    = node.x + gap;
            ly    = node.y;
            align = 'left';
          }
          drawLabel(ctx, node.id.replaceAll('-', ' '), lx, ly, align, isRim ? RENDER.labelFontSize + 1 : RENDER.labelFontSize, globalScale,
            isActive ? activeColor : COLORS[node.type] ?? '#ccc', backdrop);
        }

        ctx.globalAlpha = 1;
        node.__r = r;
      }, [activeId, neighbours, searchMatchIds, theme]);

      return <>
        <NavBar activeHref="/knowledge-graph/">
          <SearchBox nodes={graph.nodes} searchQuery={searchQuery}
                     setSearchQuery={setSearchQuery} setFocusedId={setFocusedId}
                     onFocus={() => setSearchFocused(true)}
                     onBlur={() => setSearchFocused(false)} />
          <ThemeToggle theme={theme} toggleTheme={toggleTheme} />
        </NavBar>

        {!activeNode && !searchFocused && <HeroPanel theme={theme} />}

        {!searchFocused && <CornerPanel node={activeNode} theme={theme}>
          <DetailHeader typeLabel={TYPE_LABELS[activeNode?.type]} nodeId={activeNode?.id?.replaceAll('-', ' ')} theme={theme} />
          {activeNode?.intent && <>
            <div className="info-label">Intent</div>
            <p className="info-text">{activeNode.intent}</p>
          </>}
          {activeNode?.decision && <>
            <div className="info-label">Decision</div>
            <p className="info-text">{activeNode.decision}</p>
          </>}
          {activeNode?.why && <>
            <div className="info-label">Why</div>
            <p className="info-text">{activeNode.why}</p>
          </>}
          {outLinks.length > 0 && <>
            <div className="info-label" style={{ color: OUTBOUND_COLOR }}>Outbound Links →</div>
            <div className="code-block" style={{ display: 'flex', flexDirection: 'column', gap: 8, overflowX: 'hidden', whiteSpace: 'normal' }}>
              {outLinks.map((l, i) => {
                const [, targetId] = linkEnds(l);
                return (
                  <div key={i}>
                    <span style={{ color: OUTBOUND_COLOR, fontFamily: FONT_MONO, fontSize: 13, cursor: 'pointer' }}
                          onClick={() => setFocusedId(targetId)}>→ {targetId.replaceAll('-', ' ')}</span>
                    {l.note && <div style={{ color: '#94a3b8', fontFamily: FONT_MONO, fontSize: 12, marginTop: 2 }}>{l.note}</div>}
                  </div>
                );
              })}
            </div>
          </>}
          {inLinks.length > 0 && <>
            <div className="info-label" style={{ color: INBOUND_COLOR }}>Inbound Links ←</div>
            <div className="code-block" style={{ display: 'flex', flexDirection: 'column', gap: 8, overflowX: 'hidden', whiteSpace: 'normal' }}>
              {inLinks.map((l, i) => {
                const [sourceId] = linkEnds(l);
                return (
                  <div key={i}>
                    <span style={{ color: INBOUND_COLOR, fontFamily: FONT_MONO, fontSize: 13, cursor: 'pointer' }}
                          onClick={() => setFocusedId(sourceId)}>← {sourceId.replaceAll('-', ' ')}</span>
                    {l.note && <div style={{ color: '#94a3b8', fontFamily: FONT_MONO, fontSize: 12, marginTop: 2 }}>{l.note}</div>}
                  </div>
                );
              })}
            </div>
          </>}
        </CornerPanel>}

        <ForceGraph2D
          ref={fgRef}
          width={size.width}
          height={size.height}
          graphData={graph}
          linkColor={linkColor}
          linkDirectionalArrowLength={0}
          {...physicsProps}
          backgroundColor={bgColor}
          onNodeHover={node => setHoveredId(node?.id ?? null)}
          onNodeClick={onNodeClick}
          onBackgroundClick={handleBackgroundClick}
          nodeCanvasObject={paintNode}
          nodePointerAreaPaint={nodePointerAreaPaint}
        />
      </>;
    }

    // ─── mount ───────────────────────────────────────────────────────────────

    createRoot(document.getElementById('graph')).render(<Graph />);
