> ## Documentation Index
> Fetch the complete documentation index at: https://docs.walletwall.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Knowledge Explorer

> An interactive map of WalletWall concepts — what to learn first, what each feature builds on, and where to go next.

export const KG_PRIMARY_EDGE_TYPES = Object.freeze(['requires', 'leads-to']);

export const KG_TYPES = Object.freeze({
  concept: {
    label: 'Concept',
    light: {
      fill: '#FAFAF0',
      stroke: '#B87333',
      ink: '#2B2118'
    },
    dark: {
      fill: '#241C17',
      stroke: '#8A5A1F',
      ink: '#F2E9DF'
    }
  },
  feature: {
    label: 'Feature',
    light: {
      fill: '#B84923',
      stroke: '#6B2412',
      ink: '#FFF7E8'
    },
    dark: {
      fill: '#D9714B',
      stroke: '#8A5A1F',
      ink: '#1B1512'
    }
  },
  architecture: {
    label: 'Architecture',
    light: {
      fill: '#6F7068',
      stroke: '#3C3D38',
      ink: '#FFF7E8'
    },
    dark: {
      fill: '#4A3B30',
      stroke: '#8A756A',
      ink: '#F2E9DF'
    }
  },
  security: {
    label: 'Security',
    light: {
      fill: '#5A2418',
      stroke: '#2B110B',
      ink: '#FFF7E8'
    },
    dark: {
      fill: '#7A2F21',
      stroke: '#3A150E',
      ink: '#F2E9DF'
    }
  },
  reference: {
    label: 'Reference',
    light: {
      fill: '#A7B0B5',
      stroke: '#59646A',
      ink: '#172014'
    },
    dark: {
      fill: '#59646A',
      stroke: '#3C3D38',
      ink: '#F2E9DF'
    }
  }
});

export const KG_DOMAINS = Object.freeze({
  overview: 'Overview',
  'vault-migration': 'Vault & Migration Readiness',
  'proof-readiness': 'Proof Readiness / PQC',
  'quantum-exposure': 'Quantum Exposure',
  'holder-wallet-intelligence': 'Holder & Wallet Intelligence',
  'wallet-evidence-risk': 'Wallet Evidence & Risk',
  'data-architecture': 'Data Foundation & Architecture',
  security: 'Security',
  'developer-reference': 'Developer Reference'
});

export const KG_EDGES = Object.freeze([{
  from: 'what-is-walletwall',
  to: 'introduction',
  type: 'requires'
}, {
  from: 'public-and-private',
  to: 'what-is-walletwall',
  type: 'requires'
}, {
  from: 'architecture-flows',
  to: 'what-is-walletwall',
  type: 'requires'
}, {
  from: 'vault',
  to: 'what-is-walletwall',
  type: 'requires'
}, {
  from: 'stable-seer',
  to: 'what-is-walletwall',
  type: 'requires'
}, {
  from: 'holder-wall',
  to: 'what-is-walletwall',
  type: 'requires'
}, {
  from: 'quantum-intelligence',
  to: 'what-is-walletwall',
  type: 'requires'
}, {
  from: 'what-is-walletwall',
  to: 'scoring-methodology',
  type: 'leads-to'
}, {
  from: 'scoring-methodology',
  to: 'quantum-intelligence',
  type: 'leads-to'
}, {
  from: 'quantum-intelligence',
  to: 'quantum-exposure-clock',
  type: 'leads-to'
}, {
  from: 'quantum-intelligence',
  to: 'exposure-report',
  type: 'leads-to'
}, {
  from: 'key-management-recovery',
  to: 'migration-readiness',
  type: 'leads-to'
}, {
  from: 'quantum-intelligence',
  to: 'migration-readiness',
  type: 'leads-to'
}, {
  from: 'key-management-recovery',
  to: 'vault-boundaries',
  type: 'leads-to'
}, {
  from: 'vault',
  to: 'vault-boundaries',
  type: 'leads-to'
}, {
  from: 'vault-boundaries',
  to: 'pq-evidence-rehearsal',
  type: 'leads-to'
}, {
  from: 'pq-evidence-rehearsal',
  to: 'zk-proof-artifact-roadmap',
  type: 'leads-to'
}, {
  from: 'proof-readiness-stack',
  to: 'proof-claim-schema',
  type: 'leads-to'
}, {
  from: 'proof-claim-schema',
  to: 'sp1-candidate-decision',
  type: 'leads-to'
}, {
  from: 'sp1-candidate-decision',
  to: 'sp1-dev-runbook',
  type: 'leads-to'
}, {
  from: 'zk-proof-artifact-roadmap',
  to: 'sp1-dev-runbook',
  type: 'leads-to'
}, {
  from: 'data-quality',
  to: 'wallet-graph-contract',
  type: 'leads-to'
}, {
  from: 'data-quality',
  to: 'model-layer',
  type: 'leads-to'
}, {
  from: 'model-layer',
  to: 'signal-engine',
  type: 'leads-to'
}, {
  from: 'wallet-graph-contract',
  to: 'wallet-evidence-model',
  type: 'leads-to'
}, {
  from: 'model-layer',
  to: 'wallet-evidence-model',
  type: 'leads-to'
}, {
  from: 'wallet-evidence-model',
  to: 'wallet-risk-report-model',
  type: 'leads-to'
}, {
  from: 'model-layer',
  to: 'wallet-risk-report-model',
  type: 'leads-to'
}, {
  from: 'signal-engine',
  to: 'whale-watcher',
  type: 'leads-to'
}, {
  from: 'wallet-profile',
  to: 'whale-watcher',
  type: 'leads-to'
}, {
  from: 'holder-wall',
  to: 'coinstellation',
  type: 'leads-to'
}, {
  from: 'whale-watcher',
  to: 'coinstellation',
  type: 'leads-to'
}, {
  from: 'quantum-intelligence',
  to: 'dune-queries',
  type: 'uses'
}, {
  from: 'holder-wall',
  to: 'dune-queries',
  type: 'uses'
}, {
  from: 'whale-watcher',
  to: 'dune-queries',
  type: 'uses'
}, {
  from: 'stable-seer',
  to: 'vault',
  type: 'related'
}, {
  from: 'coinstellation',
  to: 'quantum-intelligence',
  type: 'related'
}, {
  from: 'migration-readiness',
  to: 'vault',
  type: 'related'
}, {
  from: 'wallet-profile',
  to: 'vault',
  type: 'related'
}, {
  from: 'vault',
  to: 'pq-evidence-rehearsal',
  type: 'implements'
}]);

export const KG_NODES = Object.freeze([{
  id: 'introduction',
  label: 'Introduction',
  type: 'concept',
  domain: 'overview',
  path: 'introduction',
  summary: 'The product overview: a read-only, wallet-intelligence platform that turns public Ethereum data into exposure assessments and a migration-readiness path.'
}, {
  id: 'what-is-walletwall',
  label: 'What is WalletWall?',
  type: 'concept',
  domain: 'overview',
  path: 'concepts/what-is-walletwall',
  summary: "WalletWall's four exposure types, target audiences, and the scan → assess → recommend → report → monitor workflow — plus what the product explicitly is not."
}, {
  id: 'public-and-private',
  label: 'Public and Private Repositories',
  type: 'concept',
  domain: 'overview',
  path: 'concepts/public-and-private',
  summary: 'What WalletWall publishes as open research (docs, vault prototype, fixtures) versus what stays private (production app, scoring weights, Dune queries, keys).'
}, {
  id: 'architecture-flows',
  label: 'WalletWall Flows',
  type: 'architecture',
  domain: 'overview',
  path: 'architecture/flows',
  summary: 'Diagrams of the product boundary between Stable Seer, Holder Wall, and Coinstellation, the Holder Wall preflight flow, and CORS/API security posture.'
}, {
  id: 'vault',
  label: 'Stablecoin Vault & Vault Simulator',
  type: 'feature',
  domain: 'vault-migration',
  path: 'features/vault',
  summary: 'The Stablecoin Vault readiness journey (Monitor / Prepare / Testnet Rehearsal / Not Enough Data) and the Sepolia Vault Simulator research prototype.'
}, {
  id: 'migration-readiness',
  label: 'Migration Readiness',
  type: 'concept',
  domain: 'vault-migration',
  path: 'concepts/migration-readiness',
  summary: 'Normalizes existing wallet signals into four shared risk tiers (Monitor, Review, Migrate, Vault Candidate) with suggested next actions — a presentation layer, not a new score.'
}, {
  id: 'vault-boundaries',
  label: 'Vault Boundaries & Disclosures',
  type: 'security',
  domain: 'vault-migration',
  path: 'vault/boundaries',
  summary: 'What the Vault prototype does not do — no custody, no key storage, no signing authority, no mainnet deposits — and the six gates required before any of that changes.'
}, {
  id: 'pq-evidence-rehearsal',
  label: 'PQ Evidence & Testnet Rehearsal',
  type: 'security',
  domain: 'proof-readiness',
  path: 'vault/pq-evidence-and-rehearsal',
  summary: 'Three independent, read-only gates the Vault renders — PQ verifier evidence, proof-artifact status, and testnet rehearsal status — plus disclosure-only ZK/SP1 research status.'
}, {
  id: 'proof-readiness-stack',
  label: 'Proof-Readiness Architecture',
  type: 'architecture',
  domain: 'proof-readiness',
  path: 'architecture/proof-readiness-stack',
  summary: "The map and vocabulary for WalletWall's proof-readiness pipeline — what's claimed, what's gated, and what no proof exists for yet. “proof-ready, not SP1-dependent.”"
}, {
  id: 'proof-claim-schema',
  label: 'Proof Claim Schema',
  type: 'architecture',
  domain: 'proof-readiness',
  path: 'architecture/proof-claim-schema',
  summary: 'The versioned, typed contract a future proof or attestation claim must satisfy to attach to a Readiness Packet — introduces no prover, verifier, or contract code itself.'
}, {
  id: 'sp1-candidate-decision',
  label: 'SP1 Candidate Decision',
  type: 'architecture',
  domain: 'proof-readiness',
  path: 'architecture/sp1-candidate-decision',
  summary: 'The architecture decision recording SP1 as a research-only candidate zkVM for a dev scaffold — explicitly not a production commitment or platform dependency.'
}, {
  id: 'zk-proof-artifact-roadmap',
  label: 'ZK Proof-Artifact Roadmap',
  type: 'architecture',
  domain: 'proof-readiness',
  path: 'vault/zk-proof-artifact-roadmap',
  summary: "A six-stage, independently-gated roadmap for the public vault repo's PQ verifier and ZK/SP1 work — most stages done in research, proving/e2e still unbuilt."
}, {
  id: 'quantum-intelligence',
  label: 'Quantum Intelligence',
  type: 'feature',
  domain: 'quantum-exposure',
  path: 'features/quantum-intelligence',
  summary: 'The formal Quantum Exposure Score — four weighted signal groups plus a migration-readiness offset — surfaced inside Holder Wall, Whale Watcher, and Coinstellation.'
}, {
  id: 'quantum-exposure-clock',
  label: 'Quantum Exposure Clock',
  type: 'concept',
  domain: 'quantum-exposure',
  path: 'concepts/quantum-exposure-clock',
  summary: 'A wallet-specific reading of long-horizon signature exposure derived from the Quantum Exposure Score — explicitly not a Q-Day countdown.'
}, {
  id: 'exposure-report',
  label: 'Exposure Report Methodology',
  type: 'concept',
  domain: 'quantum-exposure',
  path: 'concepts/exposure-report',
  summary: 'A recurring cohort-level snapshot of signature exposure and dormancy across tracked stablecoin holders, computed deterministically from an existing cached Dune snapshot.'
}, {
  id: 'scoring-methodology',
  label: 'Scoring Methodology',
  type: 'concept',
  domain: 'quantum-exposure',
  path: 'concepts/scoring-methodology',
  summary: 'The plain-English walkthrough of wallet exposure scoring — the same five weighted signal groups as Quantum Intelligence, with a worked example.'
}, {
  id: 'holder-wall',
  label: 'Holder Wall',
  type: 'feature',
  domain: 'holder-wallet-intelligence',
  path: 'features/holder-wall',
  summary: 'A zoomable treemap of top token holders with rank deltas, a leaderboard, and a wallet drawer that can hand off to Vault readiness.'
}, {
  id: 'whale-watcher',
  label: 'Whale Watcher',
  type: 'feature',
  domain: 'holder-wallet-intelligence',
  path: 'features/whale-watcher',
  summary: 'Deep, single-wallet readiness review — activity heatmap, behavioral signal panel, AI-assisted narrative, holdings strip — built on the signal engine.'
}, {
  id: 'stable-seer',
  label: 'Stable Seer',
  type: 'feature',
  domain: 'holder-wallet-intelligence',
  path: 'features/stable-seer',
  summary: 'Stablecoin peg risk, liquidity depth, and issuer concentration across DEX pairs — an intelligence-only input to Vault readiness, not a holder-analytics surface.'
}, {
  id: 'coinstellation',
  label: 'Coinstellation',
  type: 'feature',
  domain: 'holder-wallet-intelligence',
  path: 'features/coinstellation',
  summary: 'An interactive wallet relationship graph of on-chain connections and transaction paths, opened from a Holder Wall tile or a Whale Watcher wallet.'
}, {
  id: 'wallet-profile',
  label: 'Wallet Profile',
  type: 'feature',
  domain: 'holder-wallet-intelligence',
  path: 'features/wallet-profile',
  summary: 'A compact single-wallet overview (balance, holdings, transfer flow) that hands off to the heavier Whale Watcher workspace for full signal analysis.'
}, {
  id: 'model-layer',
  label: 'Model Layer',
  type: 'architecture',
  domain: 'data-architecture',
  path: 'architecture/model-layer',
  summary: 'The shared, dependency-free schema layer — data shapes, source provenance, and quality metadata — every WalletWall feature builds on.'
}, {
  id: 'signal-engine',
  label: 'Signal Engine',
  type: 'architecture',
  domain: 'data-architecture',
  path: 'architecture/signal-engine',
  summary: 'A deterministic fact layer that turns a Dune baseline plus live wallet events into a typed WalletSignal[] taxonomy, which AI narratives must not contradict.'
}, {
  id: 'data-quality',
  label: 'Data Quality & Observability',
  type: 'architecture',
  domain: 'data-architecture',
  path: 'architecture/data-quality',
  summary: 'The trust-model reference — provider-selection logic, the observability/dataQuality field set, and the source-confidence map behind the Wallet Graph and Stable Seer.'
}, {
  id: 'wallet-graph-contract',
  label: 'Wallet Graph Response Contract',
  type: 'architecture',
  domain: 'wallet-evidence-risk',
  path: 'architecture/wallet-graph-contract',
  summary: 'The WalletGraph.v1 response contract for the wallet-lookup endpoint — required fields, the fingerprint score object, and node/edge shapes.'
}, {
  id: 'wallet-evidence-model',
  label: 'Wallet Evidence Model',
  type: 'architecture',
  domain: 'wallet-evidence-risk',
  path: 'architecture/wallet-evidence-model',
  summary: 'A provenance-aware transform that normalizes provider outputs into five source-aware claims with an explicit ValueState, so unavailable data never renders as “$0”.'
}, {
  id: 'wallet-risk-report-model',
  label: 'Wallet Risk Report Model',
  type: 'architecture',
  domain: 'wallet-evidence-risk',
  path: 'architecture/wallet-risk-report-model',
  summary: "A read-only composition layer combining four existing scorers into one per-wallet risk report, with “unknown is never zero” honesty rules enforced by tests."
}, {
  id: 'dune-queries',
  label: 'Dune Queries',
  type: 'reference',
  domain: 'data-architecture',
  path: 'development/dune-queries',
  summary: "Reference for WalletWall's scheduled (never live) Dune Analytics queries — field contracts, cadence, and staleness thresholds by feature."
}, {
  id: 'key-management-recovery',
  label: 'Key Management & Recovery Model',
  type: 'security',
  domain: 'security',
  path: 'security/key-management-recovery-model',
  summary: 'The non-custodial guardrails: WalletWall never stores keys or asks for seed phrases; the orchestration layer today only classifies and recommends — it never signs or writes.'
}, {
  id: 'sp1-dev-runbook',
  label: 'SP1 Dev Environment Runbook',
  type: 'reference',
  domain: 'developer-reference',
  path: 'operations/sp1-dev-environment-runbook',
  summary: 'Local, gated setup steps for the SP1 research scaffold — execute-only (no proof generated), off the application runtime path.'
}]);

export const KnowledgeExplorer = ({idPrefix = 'kge'}) => {
  const STYLES = `
.ww-kge{--ww-kge-bg:#FAFAF0;--ww-kge-panel:#FFF7E8;--ww-kge-ink:#2B2118;--ww-kge-muted:#59646A;--ww-kge-line:#B87333;--ww-kge-line-soft:#D9C7B3;--ww-kge-accent:#B84923;--ww-kge-edge:#8A5A1F;--ww-kge-edge-soft:#C9B79E;--ww-kge-grid:rgba(184,115,51,0.09);}
@media (prefers-color-scheme:dark){.ww-kge{--ww-kge-bg:#1B1512;--ww-kge-panel:#241C17;--ww-kge-ink:#F2E9DF;--ww-kge-muted:#A7B0B5;--ww-kge-line:#8A5A1F;--ww-kge-line-soft:#4A3B30;--ww-kge-accent:#D9714B;--ww-kge-edge:#D6A85A;--ww-kge-edge-soft:#5A4B3C;--ww-kge-grid:rgba(214,168,90,0.07);}}
.light .ww-kge,:root.light .ww-kge{--ww-kge-bg:#FAFAF0;--ww-kge-panel:#FFF7E8;--ww-kge-ink:#2B2118;--ww-kge-muted:#59646A;--ww-kge-line:#B87333;--ww-kge-line-soft:#D9C7B3;--ww-kge-accent:#B84923;--ww-kge-edge:#8A5A1F;--ww-kge-edge-soft:#C9B79E;--ww-kge-grid:rgba(184,115,51,0.09);}
.dark .ww-kge,:root.dark .ww-kge{--ww-kge-bg:#1B1512;--ww-kge-panel:#241C17;--ww-kge-ink:#F2E9DF;--ww-kge-muted:#A7B0B5;--ww-kge-line:#8A5A1F;--ww-kge-line-soft:#4A3B30;--ww-kge-accent:#D9714B;--ww-kge-edge:#D6A85A;--ww-kge-edge-soft:#5A4B3C;--ww-kge-grid:rgba(214,168,90,0.07);}
.ww-kge{position:relative;background:var(--ww-kge-bg);color:var(--ww-kge-ink);border:1px solid var(--ww-kge-line);border-radius:3px;padding:1.1rem;font-size:0.875rem;line-height:1.5;background-image:radial-gradient(var(--ww-kge-grid) 0.6px,transparent 0.6px);background-size:10px 10px;}
.ww-kge *{box-sizing:border-box;}
.ww-kge-h{font-weight:600;font-size:0.75rem;letter-spacing:0.14em;text-transform:uppercase;color:var(--ww-kge-muted);margin:0 0 0.85rem;display:flex;align-items:center;gap:0.5rem;}
.ww-kge-h::before{content:'';display:inline-block;width:0.5rem;height:0.5rem;background:var(--ww-kge-accent);transform:rotate(45deg);flex:none;}
.ww-kge-visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
.ww-kge-controls{display:flex;flex-direction:column;gap:0.65rem;margin-bottom:0.9rem;}
.ww-kge-row{display:flex;flex-wrap:wrap;align-items:center;gap:0.5rem;}
.ww-kge-row-divided{border-top:1px solid var(--ww-kge-line-soft);padding-top:0.6rem;}
.ww-kge-search{flex:1 1 14rem;min-width:10rem;padding:0.4rem 0.6rem;border:1px solid var(--ww-kge-line-soft);border-radius:2px;background:var(--ww-kge-panel);color:var(--ww-kge-ink);font:inherit;}
.ww-kge-search:focus-visible{outline:2px solid var(--ww-kge-accent);outline-offset:2px;}
.ww-kge-chip{padding:0.3rem 0.65rem;border:1px solid var(--ww-kge-line-soft);border-radius:999px;background:var(--ww-kge-panel);color:var(--ww-kge-ink);font:inherit;font-size:0.75rem;cursor:pointer;transition:border-color 120ms ease,background 120ms ease;}
.ww-kge-chip[aria-pressed="true"]{background:var(--ww-kge-accent);border-color:var(--ww-kge-accent);color:var(--ww-kge-bg);font-weight:600;}
.ww-kge-chip:focus-visible,.ww-kge-btn:focus-visible,.ww-kge-node:focus-visible,.ww-kge-toggle:focus-visible,.ww-kge-list-item:focus-visible{outline:2px solid var(--ww-kge-accent);outline-offset:2px;}
.ww-kge-btn{padding:0.3rem 0.7rem;border:1px solid var(--ww-kge-line);border-radius:2px;background:transparent;color:var(--ww-kge-ink);font:inherit;font-size:0.8125rem;cursor:pointer;}
.ww-kge-btn:disabled{opacity:0.4;cursor:not-allowed;}
.ww-kge-toggle{display:inline-flex;align-items:center;gap:0.4rem;font-size:0.8125rem;color:var(--ww-kge-muted);}
.ww-kge-legend{display:flex;flex-wrap:wrap;gap:0.75rem;font-size:0.6875rem;letter-spacing:0.02em;color:var(--ww-kge-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
.ww-kge-swatch{display:inline-block;width:0.55rem;height:0.55rem;margin-right:0.35rem;vertical-align:-0.03rem;border:1px solid var(--ww-kge-line-soft);}
.ww-kge-view{display:block;}
.ww-kge-view-list{display:none;}
@media (max-width:47.9375rem){.ww-kge-view{display:none;}.ww-kge-view-list{display:block;}}
@media (min-width:60rem){.ww-kge-list{max-height:32rem;}.ww-kge-search{flex-basis:20rem;}}
.ww-kge-force-graph .ww-kge-view{display:block !important;}
.ww-kge-force-graph .ww-kge-view-list{display:none !important;}
.ww-kge-force-list .ww-kge-view{display:none !important;}
.ww-kge-force-list .ww-kge-view-list{display:block !important;}
.ww-kge-scroll{overflow-x:auto;border:1px solid var(--ww-kge-line-soft);border-radius:2px;background:var(--ww-kge-bg);background-image:linear-gradient(var(--ww-kge-grid) 1px,transparent 1px),linear-gradient(90deg,var(--ww-kge-grid) 1px,transparent 1px);background-size:24px 24px;}
.ww-kge-canvas-outer{position:relative;}
.ww-kge-zoom-row{display:flex;align-items:center;gap:0.4rem;padding:0.5rem 0.6rem;border-bottom:1px solid var(--ww-kge-line-soft);background:var(--ww-kge-panel);}
.ww-kge-zoom-label{font-size:0.75rem;color:var(--ww-kge-muted);min-width:3.5rem;text-align:center;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
.ww-kge-canvas{position:relative;}
.ww-kge-col-header{position:absolute;top:0;transform:translateX(-50%);text-align:center;pointer-events:none;}
.ww-kge-col-header-label{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.625rem;letter-spacing:0.12em;text-transform:uppercase;color:var(--ww-kge-muted);}
.ww-kge-col-header-sub{display:block;font-size:0.5625rem;letter-spacing:0.06em;text-transform:uppercase;color:var(--ww-kge-accent);font-weight:600;margin-top:0.1rem;}
.ww-kge-svg{position:absolute;top:0;left:0;pointer-events:none;overflow:visible;}
.ww-kge-edge{fill:none;stroke:var(--ww-kge-edge);stroke-width:1.4;opacity:0.7;stroke-dasharray:600;stroke-dashoffset:0;animation:ww-kge-draw 480ms ease-out both;animation-delay:var(--kge-delay,0ms);}
.ww-kge-edge-secondary{fill:none;stroke:var(--ww-kge-edge-soft);stroke-width:1.1;stroke-dasharray:4 3;opacity:0.85;}
.ww-kge-edge-secondary.ww-kge-edge-anim{stroke-dasharray:600;stroke-dashoffset:0;animation:ww-kge-draw 420ms ease-out both;}
.ww-kge-edge-active{stroke:var(--ww-kge-accent) !important;stroke-width:2.6 !important;opacity:1 !important;stroke-dasharray:none !important;}
.ww-kge-edge-dimmed{opacity:0.12 !important;}
@media (prefers-reduced-motion:no-preference){.ww-kge-edge,.ww-kge-edge-secondary{transition:opacity 160ms ease,stroke 160ms ease,stroke-width 160ms ease;}}
@keyframes ww-kge-draw{from{stroke-dashoffset:600;}to{stroke-dashoffset:0;}}
.ww-kge-node{position:absolute;transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:0.15rem;padding:0.42rem 0.55rem;border-width:1px;border-left-width:3px;border-style:solid;border-radius:2px;font:inherit;text-align:left;cursor:pointer;background:var(--ww-kge-panel);border-color:var(--kge-stroke-light);opacity:1;animation:ww-kge-rise 340ms cubic-bezier(.16,.8,.3,1) both;animation-delay:var(--kge-delay,0ms);}
.ww-kge-node::before{content:'';position:absolute;top:-1px;left:-1px;border-style:solid;border-width:0 0 11px 11px;border-color:transparent transparent var(--kge-stroke-light) transparent;}
@media (prefers-color-scheme:dark){.ww-kge-node{background:var(--ww-kge-panel);border-color:var(--kge-stroke-dark);}.ww-kge-node::before{border-color:transparent transparent var(--kge-stroke-dark) transparent;}}
.light .ww-kge-node{background:var(--ww-kge-panel);border-color:var(--kge-stroke-light);}
.light .ww-kge-node::before{border-color:transparent transparent var(--kge-stroke-light) transparent;}
.dark .ww-kge-node{background:var(--ww-kge-panel);border-color:var(--kge-stroke-dark);}
.dark .ww-kge-node::before{border-color:transparent transparent var(--kge-stroke-dark) transparent;}
@media (prefers-reduced-motion:no-preference){.ww-kge-node{transition:transform 140ms ease,box-shadow 140ms ease;}.ww-kge-node:hover,.ww-kge-node:focus-visible{transform:translate(-50%,-50%) translateY(-3px);box-shadow:0 6px 14px rgba(20,14,8,0.22);}}
@media (prefers-reduced-motion:reduce){.ww-kge-node:hover,.ww-kge-node:focus-visible{border-left-width:5px;}}
.ww-kge-node[data-dim="true"]{opacity:0.28;}
.ww-kge-node[aria-pressed="true"]{border-left-width:5px;box-shadow:0 0 0 1px var(--kge-stroke-light);}
.dark .ww-kge-node[aria-pressed="true"]{box-shadow:0 0 0 1px var(--kge-stroke-dark);}
.ww-kge-node-tag{position:absolute;top:0.2rem;right:0.35rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.5625rem;color:var(--ww-kge-muted);letter-spacing:0.02em;}
.ww-kge-node-label{font-weight:600;font-size:0.75rem;line-height:1.2;padding-right:1.6rem;}
.ww-kge-node-meta{font-size:0.625rem;opacity:0.85;}
@media (prefers-reduced-motion:reduce){.ww-kge-node,.ww-kge-edge,.ww-kge-edge-secondary.ww-kge-edge-anim{animation:none !important;}}
.ww-kge-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:0.4rem;max-height:26rem;overflow-y:auto;}
.ww-kge-list-item{width:100%;text-align:left;padding:0.5rem 0.65rem 0.5rem 0.7rem;border:1px solid var(--ww-kge-line-soft);border-left:3px solid var(--ww-kge-line-soft);border-radius:2px;background:var(--ww-kge-panel);color:var(--ww-kge-ink);font:inherit;cursor:pointer;}
.ww-kge-list-item[data-dim="true"]{opacity:0.4;}
.ww-kge-list-item[aria-pressed="true"]{border-left-color:var(--ww-kge-accent);border-left-width:5px;}
.ww-kge-panel{margin-top:0.9rem;border:1px solid var(--ww-kge-line-soft);border-radius:2px;background:var(--ww-kge-panel);overflow:hidden;}
.ww-kge-panel-tab{height:5px;background:var(--kge-tab-light,var(--ww-kge-accent));}
@media (prefers-color-scheme:dark){.ww-kge-panel-tab{background:var(--kge-tab-dark,var(--ww-kge-accent));}}
.dark .ww-kge-panel-tab{background:var(--kge-tab-dark,var(--ww-kge-accent));}
.light .ww-kge-panel-tab{background:var(--kge-tab-light,var(--ww-kge-accent));}
.ww-kge-panel-body{padding:0.9rem;}
.ww-kge-panel h3{margin:0 0 0.15rem;font-size:1.0625rem;letter-spacing:-0.01em;}
.ww-kge-panel-meta{font-size:0.6875rem;letter-spacing:0.06em;text-transform:uppercase;color:var(--ww-kge-muted);margin-bottom:0.6rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
.ww-kge-panel dl{display:grid;grid-template-columns:1fr;gap:0.15rem 0;margin:0.6rem 0 0;}
.ww-kge-panel dt{font-weight:600;color:var(--ww-kge-muted);font-size:0.6875rem;text-transform:uppercase;letter-spacing:0.08em;margin-top:0.6rem;}
.ww-kge-panel dd{margin:0.2rem 0 0;}
.ww-kge-inline-list{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:0.35rem;}
.ww-kge-inline-btn{padding:0.2rem 0.5rem;border:1px solid var(--ww-kge-line-soft);border-radius:999px;background:var(--ww-kge-bg);color:var(--ww-kge-ink);font:inherit;font-size:0.75rem;cursor:pointer;}
.ww-kge-chain{list-style:none;display:flex;flex-wrap:wrap;align-items:center;gap:0.3rem;font-size:0.8125rem;margin:0;padding:0;}
.ww-kge-chain li{display:flex;align-items:center;gap:0.3rem;}
.ww-kge-chain li:not(:first-child)::before{content:'→';color:var(--ww-kge-muted);}
.ww-kge-count{font-size:0.75rem;color:var(--ww-kge-muted);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;}
.ww-kge-empty-banner{padding:0.6rem 0.8rem;margin-bottom:0.6rem;border:1px dashed var(--ww-kge-line-soft);border-radius:2px;color:var(--ww-kge-muted);font-size:0.8125rem;background:var(--ww-kge-panel);}
.ww-kge-panel-actions{display:flex;flex-wrap:wrap;gap:0.5rem;margin-top:0.8rem;padding-top:0.7rem;border-top:1px dashed var(--ww-kge-line-soft);}
.ww-kge-link{color:var(--ww-kge-accent);font-weight:600;text-decoration:underline;}
.ww-kge-empty{color:var(--ww-kge-muted);font-size:0.8125rem;padding:0.5rem 0;}
`;
  const normalizePrereqEdges = edges => edges.filter(e => e.type === 'requires' || e.type === 'leads-to').map(e => e.type === 'requires' ? {
    prereq: e.to,
    dependent: e.from
  } : {
    prereq: e.from,
    dependent: e.to
  });
  const getNode = id => KG_NODES.find(n => n.id === id);
  const prerequisitesOf = id => normalizePrereqEdges(KG_EDGES).filter(e => e.dependent === id).map(e => e.prereq);
  const nextConceptsOf = id => normalizePrereqEdges(KG_EDGES).filter(e => e.prereq === id).map(e => e.dependent);
  const relatedOf = id => KG_EDGES.filter(e => e.type !== 'requires' && e.type !== 'leads-to').filter(e => e.from === id || e.to === id).map(e => ({
    id: e.from === id ? e.to : e.from,
    type: e.type
  }));
  const HEADER_HEIGHT = 34;
  const computeLayout = (nodes, edges) => {
    const colWidth = 196;
    const rowHeight = 92;
    const nodeWidth = 168;
    const nodeHeight = 54;
    const pad = 24;
    const prereqOf = {};
    normalizePrereqEdges(edges).forEach(({prereq, dependent}) => {
      (prereqOf[dependent] ??= []).push(prereq);
    });
    const depth = {};
    const visiting = new Set();
    const resolveDepth = id => {
      if (depth[id] !== undefined) return depth[id];
      if (visiting.has(id)) return 0;
      visiting.add(id);
      const prereqs = prereqOf[id] ?? [];
      const d = prereqs.length === 0 ? 0 : 1 + Math.max(...prereqs.map(resolveDepth));
      visiting.delete(id);
      depth[id] = d;
      return d;
    };
    nodes.forEach(n => resolveDepth(n.id));
    const byCol = {};
    nodes.forEach(n => {
      const col = depth[n.id] ?? 0;
      (byCol[col] ??= []).push(n);
    });
    Object.values(byCol).forEach(list => list.sort((a, b) => a.domain.localeCompare(b.domain) || a.label.localeCompare(b.label)));
    const positions = {};
    let maxRows = 0;
    Object.entries(byCol).forEach(([col, list]) => {
      maxRows = Math.max(maxRows, list.length);
      list.forEach((n, row) => {
        positions[n.id] = {
          col: Number(col),
          row,
          x: pad + Number(col) * colWidth + nodeWidth / 2,
          y: pad + HEADER_HEIGHT + row * rowHeight + nodeHeight / 2
        };
      });
    });
    const cols = Object.keys(byCol).map(Number).sort((a, b) => a - b);
    const maxCol = cols.length === 0 ? 0 : Math.max(...cols);
    const columns = cols.map(col => ({
      col,
      x: pad + col * colWidth + nodeWidth / 2,
      isFirst: col === 0,
      isLast: col === maxCol
    }));
    return {
      positions,
      columns,
      nodeWidth,
      nodeHeight,
      width: pad * 2 + (maxCol + 1) * colWidth,
      height: pad * 2 + HEADER_HEIGHT + maxRows * rowHeight
    };
  };
  const searchNodes = (nodes, query) => {
    const q = query.trim().toLowerCase();
    if (!q) return new Set(nodes.map(n => n.id));
    return new Set(nodes.filter(n => n.label.toLowerCase().includes(q) || n.summary.toLowerCase().includes(q) || (KG_DOMAINS[n.domain] ?? '').toLowerCase().includes(q)).map(n => n.id));
  };
  const domainList = Object.keys(KG_DOMAINS);
  const typeList = Object.keys(KG_TYPES);
  const ZOOM_LEVELS = [0.6, 0.75, 0.85, 1, 1.15, 1.3];
  const DEFAULT_ZOOM_INDEX = 2;
  const PERSIST_NAMESPACE = 'ww-kge-state:v1';
  const loadPersistedState = () => {
    try {
      if (typeof window === 'undefined' || !window.localStorage) return null;
      const raw = window.localStorage.getItem(PERSIST_NAMESPACE);
      if (!raw) return null;
      const parsed = JSON.parse(raw);
      return parsed && typeof parsed === 'object' ? parsed : null;
    } catch {
      return null;
    }
  };
  const savePersistedState = state => {
    try {
      if (typeof window === 'undefined' || !window.localStorage) return;
      window.localStorage.setItem(PERSIST_NAMESPACE, JSON.stringify(state));
    } catch {}
  };
  const clearPersistedState = () => {
    try {
      if (typeof window === 'undefined' || !window.localStorage) return;
      window.localStorage.removeItem(PERSIST_NAMESPACE);
    } catch {}
  };
  const [query, setQuery] = useState('');
  const [activeDomains, setActiveDomains] = useState(() => new Set());
  const [activeTypes, setActiveTypes] = useState(() => new Set());
  const [showRelated, setShowRelated] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  const [viewOverride, setViewOverride] = useState(null);
  const [zoomIndex, setZoomIndex] = useState(DEFAULT_ZOOM_INDEX);
  const nodeRefs = useRef({});
  const scrollRef = useRef(null);
  const layout = useMemo(() => computeLayout(KG_NODES, KG_EDGES), []);
  const matchingSearch = useMemo(() => searchNodes(KG_NODES, query), [query]);
  const isDimmed = node => {
    if (!matchingSearch.has(node.id)) return true;
    if (activeDomains.size > 0 && !activeDomains.has(node.domain)) return true;
    if (activeTypes.size > 0 && !activeTypes.has(node.type)) return true;
    return false;
  };
  const toggleDomain = domain => {
    setActiveDomains(prev => {
      const next = new Set(prev);
      if (next.has(domain)) next.delete(domain); else next.add(domain);
      return next;
    });
  };
  const toggleType = type => {
    setActiveTypes(prev => {
      const next = new Set(prev);
      if (next.has(type)) next.delete(type); else next.add(type);
      return next;
    });
  };
  const domainCount = domain => KG_NODES.filter(n => n.domain === domain).length;
  const typeCount = type => KG_NODES.filter(n => n.type === type).length;
  const chainOf = id => {
    const chain = [id];
    const seen = new Set([id]);
    let current = id;
    for (let i = 0; i < KG_NODES.length; i += 1) {
      const prereqs = prerequisitesOf(current);
      if (prereqs.length === 0) break;
      const next = prereqs[0];
      if (seen.has(next)) break;
      chain.unshift(next);
      seen.add(next);
      current = next;
    }
    return chain;
  };
  const focusNode = (id, {moveDomFocus = false} = {}) => {
    setSelectedId(id);
    const el = nodeRefs.current[id];
    if (!el) return;
    const reduceMotion = typeof window !== 'undefined' && window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false;
    el.scrollIntoView({
      behavior: reduceMotion ? 'auto' : 'smooth',
      inline: 'center',
      block: 'nearest'
    });
    if (moveDomFocus) el.focus();
  };
  const handleNodeKeyDown = (e, node) => {
    if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return;
    const pos = layout.positions[node.id];
    const inColumn = col => KG_NODES.filter(n => layout.positions[n.id].col === col);
    const nearestByRow = candidates => {
      if (candidates.length === 0) return null;
      return candidates.reduce((best, n) => {
        const bestDelta = Math.abs(layout.positions[best.id].row - pos.row);
        const nDelta = Math.abs(layout.positions[n.id].row - pos.row);
        return nDelta < bestDelta ? n : best;
      });
    };
    let target = null;
    if (e.key === 'ArrowDown') {
      target = inColumn(pos.col).find(n => layout.positions[n.id].row === pos.row + 1);
    } else if (e.key === 'ArrowUp') {
      target = inColumn(pos.col).find(n => layout.positions[n.id].row === pos.row - 1);
    } else if (e.key === 'ArrowRight') {
      target = nearestByRow(inColumn(pos.col + 1));
    } else if (e.key === 'ArrowLeft') {
      target = nearestByRow(inColumn(pos.col - 1));
    }
    if (target) {
      e.preventDefault();
      focusNode(target.id, {
        moveDomFocus: true
      });
    }
  };
  const reset = () => {
    setQuery('');
    setActiveDomains(new Set());
    setActiveTypes(new Set());
    setShowRelated(false);
    setSelectedId(null);
    setViewOverride(null);
    setZoomIndex(DEFAULT_ZOOM_INDEX);
    if (scrollRef.current) {
      scrollRef.current.scrollLeft = 0;
      scrollRef.current.scrollTop = 0;
    }
    clearPersistedState();
  };
  const zoom = ZOOM_LEVELS[zoomIndex];
  const zoomOut = () => setZoomIndex(i => Math.max(0, i - 1));
  const zoomIn = () => setZoomIndex(i => Math.min(ZOOM_LEVELS.length - 1, i + 1));
  useEffect(() => {
    const el = scrollRef.current;
    if (!el) return undefined;
    const onWheel = e => {
      if (!e.ctrlKey && !e.metaKey) return;
      e.preventDefault();
      if (e.deltaY < 0) setZoomIndex(i => Math.min(ZOOM_LEVELS.length - 1, i + 1)); else if (e.deltaY > 0) setZoomIndex(i => Math.max(0, i - 1));
    };
    el.addEventListener('wheel', onWheel, {
      passive: false
    });
    return () => el.removeEventListener('wheel', onWheel);
  }, []);
  useEffect(() => {
    const persisted = loadPersistedState();
    if (persisted) {
      if (typeof persisted.query === 'string') setQuery(persisted.query);
      if (Array.isArray(persisted.activeDomains)) {
        setActiveDomains(new Set(persisted.activeDomains.filter(d => (d in KG_DOMAINS))));
      }
      if (Array.isArray(persisted.activeTypes)) {
        setActiveTypes(new Set(persisted.activeTypes.filter(t => (t in KG_TYPES))));
      }
      if (typeof persisted.showRelated === 'boolean') setShowRelated(persisted.showRelated);
      if (persisted.viewOverride === 'graph' || persisted.viewOverride === 'list') setViewOverride(persisted.viewOverride);
      if (typeof persisted.zoomIndex === 'number' && Number.isInteger(persisted.zoomIndex) && persisted.zoomIndex >= 0 && persisted.zoomIndex < ZOOM_LEVELS.length) {
        setZoomIndex(persisted.zoomIndex);
      }
      if (typeof persisted.selectedId === 'string' && getNode(persisted.selectedId)) {
        setSelectedId(persisted.selectedId);
      }
    }
    try {
      if (typeof window !== 'undefined') {
        const params = new URLSearchParams(window.location.search);
        const concept = params.get('concept');
        if (concept && getNode(concept)) focusNode(concept);
      }
    } catch {}
  }, []);
  const skipFirstPersistRef = useRef(true);
  useEffect(() => {
    if (skipFirstPersistRef.current) {
      skipFirstPersistRef.current = false;
      return;
    }
    savePersistedState({
      query,
      activeDomains: [...activeDomains],
      activeTypes: [...activeTypes],
      showRelated,
      selectedId,
      viewOverride,
      zoomIndex
    });
    try {
      if (typeof window !== 'undefined' && window.history?.replaceState) {
        const url = new URL(window.location.href);
        if (selectedId) url.searchParams.set('concept', selectedId); else url.searchParams.delete('concept');
        window.history.replaceState(null, '', url.toString());
      }
    } catch {}
  }, [query, activeDomains, activeTypes, showRelated, selectedId, viewOverride, zoomIndex]);
  const selectedNode = selectedId ? getNode(selectedId) : null;
  const prerequisites = selectedId ? prerequisitesOf(selectedId) : [];
  const nextConcepts = selectedId ? nextConceptsOf(selectedId) : [];
  const related = selectedId ? relatedOf(selectedId) : [];
  const isEdgeActive = e => selectedId != null && (e.from === selectedId || e.to === selectedId);
  const primaryEdgesWithPos = KG_EDGES.filter(e => KG_PRIMARY_EDGE_TYPES.includes(e.type)).map(e => ({
    e,
    from: layout.positions[e.from],
    to: layout.positions[e.to]
  })).filter(p => p.from && p.to);
  const secondaryEdgesWithPos = KG_EDGES.filter(e => !KG_PRIMARY_EDGE_TYPES.includes(e.type)).map(e => ({
    e,
    from: layout.positions[e.from],
    to: layout.positions[e.to]
  })).filter(p => p.from && p.to);
  const visibleNodes = KG_NODES.filter(n => !isDimmed(n));
  const orderedNodes = [...KG_NODES].sort((a, b) => {
    const pa = layout.positions[a.id];
    const pb = layout.positions[b.id];
    return pa.col - pb.col || pa.row - pb.row;
  });
  const wrapperClass = ['ww-kge', 'not-prose', viewOverride === 'graph' ? 'ww-kge-force-graph' : '', viewOverride === 'list' ? 'ww-kge-force-list' : ''].filter(Boolean).join(' ');
  const renderDetailsPanel = () => selectedNode ? <div className="ww-kge-panel" aria-live="polite">
        <div className="ww-kge-panel-tab" style={{
    '--kge-tab-light': KG_TYPES[selectedNode.type]?.light.stroke,
    '--kge-tab-dark': KG_TYPES[selectedNode.type]?.dark.stroke
  }} />
        <div className="ww-kge-panel-body">
          <h3>{selectedNode.label}</h3>
          <p className="ww-kge-panel-meta">
            {KG_TYPES[selectedNode.type]?.label ?? selectedNode.type} · {KG_DOMAINS[selectedNode.domain] ?? selectedNode.domain} · depth {layout.positions[selectedNode.id]?.col ?? 0}
          </p>
          <p>{selectedNode.summary}</p>
          <dl>
            <dt>Learning path to here</dt>
            <dd>
              {chainOf(selectedNode.id).length <= 1 ? <span className="ww-kge-empty">This is a root concept — no prerequisites lead here.</span> : <ol className="ww-kge-chain">
                  {chainOf(selectedNode.id).map((id, i, arr) => <li key={id}>
                      {i === arr.length - 1 ? <strong>{getNode(id)?.label ?? id}</strong> : <button type="button" className="ww-kge-inline-btn" onClick={() => focusNode(id)}>
                          {getNode(id)?.label ?? id}
                        </button>}
                    </li>)}
                </ol>}
            </dd>
            <dt>Prerequisites</dt>
            <dd>
              {prerequisites.length === 0 ? <span className="ww-kge-empty">None — a good starting point.</span> : <ul className="ww-kge-inline-list">
                  {prerequisites.map(id => <li key={id}>
                      <button type="button" className="ww-kge-inline-btn" onClick={() => focusNode(id)}>
                        {getNode(id)?.label ?? id}
                      </button>
                    </li>)}
                </ul>}
            </dd>
            <dt>Recommended next</dt>
            <dd>
              {nextConcepts.length === 0 ? <span className="ww-kge-empty">None in this curated set.</span> : <ul className="ww-kge-inline-list">
                  {nextConcepts.map(id => <li key={id}>
                      <button type="button" className="ww-kge-inline-btn" onClick={() => focusNode(id)}>
                        {getNode(id)?.label ?? id}
                      </button>
                    </li>)}
                </ul>}
            </dd>
            <dt>Related capabilities</dt>
            <dd>
              {related.length === 0 ? <span className="ww-kge-empty">None recorded.</span> : <ul className="ww-kge-inline-list">
                  {related.map(r => <li key={`${r.type}-${r.id}`}>
                      {getNode(r.id)?.label ?? r.id} <em>({r.type})</em>
                    </li>)}
                </ul>}
            </dd>
          </dl>
          <div className="ww-kge-panel-actions">
            <a className="ww-kge-link" href={`/${selectedNode.path}`}>
              Open canonical page →
            </a>
            <button type="button" className="ww-kge-btn" onClick={() => setSelectedId(null)}>
              Close details
            </button>
          </div>
        </div>
      </div> : <p className="ww-kge-empty">Select a concept to see its summary, prerequisites, and next steps.</p>;
  return <div className={wrapperClass}>
      <style>{STYLES}</style>
      <p className="ww-kge-h">WalletWall knowledge explorer — {KG_NODES.length} concepts</p>

      <div className="ww-kge-controls">
        <div className="ww-kge-row">
          <label htmlFor={`${idPrefix}-search`} className="ww-kge-visually-hidden">
            Search concepts
          </label>
          <input id={`${idPrefix}-search`} type="search" className="ww-kge-search" placeholder="Search concepts (e.g. “quantum”, “vault”, “evidence”)" value={query} onChange={e => setQuery(e.target.value)} />
          <button type="button" className="ww-kge-btn" onClick={reset}>
            Reset
          </button>
          <span className="ww-kge-count">{visibleNodes.length} of {KG_NODES.length} shown</span>
          <span className="ww-kge-toggle">
            <input id={`${idPrefix}-related`} type="checkbox" checked={showRelated} onChange={e => setShowRelated(e.target.checked)} />
            <label htmlFor={`${idPrefix}-related`}>Show related connections</label>
          </span>
        </div>

        <div className="ww-kge-row ww-kge-row-divided" role="group" aria-label="Filter by domain">
          <button type="button" className="ww-kge-chip" aria-pressed={activeDomains.size === 0} onClick={() => setActiveDomains(new Set())}>
            All domains
          </button>
          {domainList.map(domain => <button key={domain} type="button" className="ww-kge-chip" aria-pressed={activeDomains.has(domain)} onClick={() => toggleDomain(domain)}>
              {KG_DOMAINS[domain]} ({domainCount(domain)})
            </button>)}
        </div>

        <div className="ww-kge-row ww-kge-row-divided" role="group" aria-label="Filter by type">
          {typeList.map(type => <button key={type} type="button" className="ww-kge-chip" aria-pressed={activeTypes.has(type)} onClick={() => toggleType(type)}>
              <span className="ww-kge-swatch" style={{
    background: KG_TYPES[type].light.fill,
    borderColor: KG_TYPES[type].light.stroke
  }} aria-hidden="true" />
              {KG_TYPES[type].label} ({typeCount(type)})
            </button>)}
        </div>

        <div className="ww-kge-row ww-kge-row-divided" role="group" aria-label="Switch view">
          <button type="button" className="ww-kge-chip" aria-pressed={viewOverride === 'graph'} onClick={() => setViewOverride(viewOverride === 'graph' ? null : 'graph')}>
            Graph view
          </button>
          <button type="button" className="ww-kge-chip" aria-pressed={viewOverride === 'list'} onClick={() => setViewOverride(viewOverride === 'list' ? null : 'list')}>
            List view
          </button>
        </div>

        {visibleNodes.length === 0 && <p className="ww-kge-empty-banner" role="status">
            No concepts match the current search and filters.{' '}
            <button type="button" className="ww-kge-inline-btn" onClick={reset}>
              Reset filters
            </button>
          </p>}
      </div>

      <div className="ww-kge-view">
        <div className="ww-kge-zoom-row" role="group" aria-label="Zoom graph">
          <button type="button" className="ww-kge-btn" onClick={zoomOut} disabled={zoomIndex === 0}>
            − Zoom out
          </button>
          <span className="ww-kge-zoom-label">{Math.round(zoom * 100)}%</span>
          <button type="button" className="ww-kge-btn" onClick={zoomIn} disabled={zoomIndex === ZOOM_LEVELS.length - 1}>
            + Zoom in
          </button>
          <span className="ww-kge-node-meta">(Ctrl/Cmd + scroll to zoom · arrow keys move between concepts)</span>
        </div>
        <div className="ww-kge-scroll" ref={scrollRef}>
          <div className="ww-kge-canvas-outer" style={{
    width: layout.width * zoom,
    height: layout.height * zoom
  }}>
          <div className="ww-kge-canvas" style={{
    width: layout.width,
    height: layout.height,
    transform: `scale(${zoom})`,
    transformOrigin: 'top left'
  }}>
            {layout.columns.map(({col, x}) => <div key={col} className="ww-kge-col-header" style={{
    left: x,
    width: layout.nodeWidth
  }}>
                <span className="ww-kge-col-header-label">Depth {col}</span>
                {col === 0 && <span className="ww-kge-col-header-sub">Start here</span>}
              </div>)}

            <svg className="ww-kge-svg" width={layout.width} height={layout.height} aria-hidden="true" focusable="false">
              {primaryEdgesWithPos.map(({e, from, to}) => {
    const midX = (from.x + to.x) / 2;
    const d = `M ${from.x} ${from.y} C ${midX} ${from.y}, ${midX} ${to.y}, ${to.x} ${to.y}`;
    const active = isEdgeActive(e);
    const cls = ['ww-kge-edge', selectedId != null && active ? 'ww-kge-edge-active' : '', selectedId != null && !active ? 'ww-kge-edge-dimmed' : ''].filter(Boolean).join(' ');
    return <path key={`${e.from}->${e.to}-${e.type}`} className={cls} d={d} style={{
      '--kge-delay': `${from.col * 70 + 90}ms`
    }} />;
  })}
              {showRelated && secondaryEdgesWithPos.map(({e, from, to}) => {
    const active = isEdgeActive(e);
    const cls = ['ww-kge-edge-secondary', 'ww-kge-edge-anim', selectedId != null && active ? 'ww-kge-edge-active' : '', selectedId != null && !active ? 'ww-kge-edge-dimmed' : ''].filter(Boolean).join(' ');
    return <line key={`${e.from}~${e.to}-${e.type}`} className={cls} x1={from.x} y1={from.y} x2={to.x} y2={to.y} />;
  })}
            </svg>

            {KG_NODES.map(node => {
    const pos = layout.positions[node.id];
    const t = KG_TYPES[node.type];
    return <button key={node.id} type="button" ref={el => {
      nodeRefs.current[node.id] = el;
    }} className="ww-kge-node" style={{
      left: pos.x,
      top: pos.y,
      width: layout.nodeWidth,
      minHeight: layout.nodeHeight,
      '--kge-delay': `${pos.col * 70}ms`,
      '--kge-fill-light': t.light.fill,
      '--kge-stroke-light': t.light.stroke,
      '--kge-ink-light': t.light.ink,
      '--kge-fill-dark': t.dark.fill,
      '--kge-stroke-dark': t.dark.stroke,
      '--kge-ink-dark': t.dark.ink
    }} data-dim={isDimmed(node) ? 'true' : 'false'} data-tone={node.type} aria-pressed={selectedId === node.id} aria-label={`${node.label} — ${KG_TYPES[node.type]?.label} — ${KG_DOMAINS[node.domain]}`} onClick={() => focusNode(node.id)} onKeyDown={e => handleNodeKeyDown(e, node)}>
                  <span className="ww-kge-node-tag">D{pos.col}</span>
                  <span className="ww-kge-node-label">{node.label}</span>
                  <span className="ww-kge-node-meta">{KG_DOMAINS[node.domain]}</span>
                </button>;
  })}
          </div>
          </div>
        </div>
        {renderDetailsPanel()}
      </div>

      <div className="ww-kge-view-list">
        <ul className="ww-kge-list" aria-label="WalletWall concepts">
          {orderedNodes.map(node => <li key={node.id}>
              <button type="button" className="ww-kge-list-item" data-dim={isDimmed(node) ? 'true' : 'false'} data-tone={node.type} aria-pressed={selectedId === node.id} aria-label={`${node.label} — ${KG_TYPES[node.type]?.label} — ${KG_DOMAINS[node.domain]}`} onClick={() => setSelectedId(node.id)}>
                <strong>{node.label}</strong> — {KG_TYPES[node.type]?.label} · {KG_DOMAINS[node.domain]}
                <br />
                <span className="ww-kge-node-meta">{node.summary}</span>
              </button>
            </li>)}
        </ul>
        {renderDetailsPanel()}
      </div>
    </div>;
};

# Knowledge Explorer

A curated map of about 30 WalletWall concepts, drawn directly from the pages in
this documentation site — not a second copy of them. Each node is a real,
published page; the lines between them are learning-path relationships
(**requires** and **leads-to**), with secondary connections (**implements**,
**uses**, **related**) available behind the "Show related connections" toggle.

Search or filter by domain, select a concept to see its summary and
prerequisites, and follow **Open canonical page** to read the real thing. This
explorer is a navigation and discovery layer — the linked pages remain the
source of truth.

<KnowledgeExplorer idPrefix="explore" />

## If the explorer above didn't load

The interactive map requires JavaScript. Here are the same core starting
points as plain links:

* [What is WalletWall?](/concepts/what-is-walletwall) — the product overview
* [Stablecoin Vault & Vault Simulator](/features/vault) — the flagship readiness journey
* [Quantum Intelligence](/features/quantum-intelligence) — the exposure scoring model
* [Holder Wall](/features/holder-wall) — the ownership and holder-analytics map
* [Wallet Evidence Model](/architecture/wallet-evidence-model) — how evidence and confidence states work
* [Data Quality and Observability](/architecture/data-quality) — the trust-model reference underneath most features
* [Public and Private Repositories](/concepts/public-and-private) — what's open source versus private

<Info>
  This page is a proof-of-concept navigation layer. It represents a curated
  subset of the documentation, not the full site — use the sidebar for
  complete coverage.
</Info>
