// Shared components: topbar, sidebar, TOC, code blocks, feedback, search, tweaks
// Note: dangerouslySetInnerHTML is used only for our own hardcoded syntax-highlighted
// code templates below — no user input ever reaches it.

const Icon = ({ name, size = 16, stroke = 1.6 }) => {
  const paths = {
    search: <><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></>,
    sun: <><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" /></>,
    moon: <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z" />,
    chev: <path d="m6 9 6 6 6-6" />,
    chevR: <path d="m9 6 6 6-6 6" />,
    chevL: <path d="m15 6-6 6 6 6" />,
    copy: <><rect x="9" y="9" width="11" height="11" rx="2" /><path d="M5 15V6a2 2 0 0 1 2-2h9" /></>,
    check: <path d="m5 12 4.5 4.5L19 7" />,
    ext: <><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5" /></>,
    arrowR: <path d="M5 12h14M13 5l7 7-7 7" />,
    arrowL: <path d="M19 12H5M11 5l-7 7 7 7" />,
    arrowUp: <path d="M7 17 17 7M8 7h9v9" />,
    bolt: <path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z" />,
    book: <><path d="M4 4v16a2 2 0 0 0 2 2h14V4" /><path d="M4 4h14v16H6a2 2 0 0 1-2-2Z" /></>,
    code: <><path d="m16 18 6-6-6-6M8 6l-6 6 6 6" /></>,
    puzzle: <path d="M19.44 12.56a2 2 0 1 0 0-3.12V7a2 2 0 0 0-2-2h-2.44a2 2 0 1 0-3.12 0H9.44a2 2 0 0 0-2 2v2.44a2 2 0 1 1 0 3.12V17a2 2 0 0 0 2 2h2.44a2 2 0 1 0 3.12 0h2.44a2 2 0 0 0 2-2z" />,
    cpu: <><rect x="5" y="5" width="14" height="14" rx="2" /><rect x="9" y="9" width="6" height="6" /><path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" /></>,
    shield: <path d="M12 2 4 6v6c0 5 3.5 9 8 10 4.5-1 8-5 8-10V6Z" />,
    zap: <path d="M13 2 3 14h9l-1 8 10-12h-9z" />,
    terminal: <><path d="m6 9 4 3-4 3" /><path d="M12 15h6" /><rect x="3" y="4" width="18" height="16" rx="2" /></>,
    link: <><path d="M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1" /><path d="M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1" /></>,
    info: <><circle cx="12" cy="12" r="9" /><path d="M12 8h.01M11 12h1v5h1" /></>,
    warn: <><path d="M12 3 2 21h20z" /><path d="M12 9v5M12 18h.01" /></>,
    sparkle: <path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M5.6 18.4l2.8-2.8M15.6 8.4l2.8-2.8" />,
    thumbUp: <path d="M7 10v11H3V10zm4 0 3-7a2 2 0 0 1 4 1l-2 6h5a2 2 0 0 1 2 2l-2 8a2 2 0 0 1-2 2h-8z" />,
    thumbDown: <path d="M17 14V3h4v11zm-4 0-3 7a2 2 0 0 1-4-1l2-6H3a2 2 0 0 1-2-2l2-8a2 2 0 0 1 2-2h8z" />,
    hash: <path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18" />,
    clock: <><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3 2" /></>,
    command: <path d="M15 6a3 3 0 1 1 3 3h-3zm0 0v12m0 0a3 3 0 1 0 3-3h-3zm0 0H9m0 0a3 3 0 1 1-3-3h3zm0 0V6m0 0a3 3 0 1 0-3 3h3z" />,
    return: <path d="m9 10-3 3 3 3M6 13h10a4 4 0 0 0 4-4V6" />,
    x: <path d="M18 6 6 18M6 6l12 12" />,
    plus: <path d="M12 5v14M5 12h14" />,
    menu: <path d="M4 6h16M4 12h16M4 18h16" />,
    rocket: <><path d="M4.5 16.5c-1.5 1.3-2 5-2 5s3.7-.5 5-2c.7-.8.7-2.1-.1-3-.8-.8-2.1-.8-2.9 0Z"/><path d="M12 15 9 12a11 11 0 0 1 7-7c3 0 6 1 6 1s0 3-1 6a11 11 0 0 1-7 7Z"/><path d="M9 12H4s.5-3 2-4 5 0 5 0M12 15v5s3-.5 4-2 0-5 0-5"/><circle cx="15" cy="9" r="1.2"/></>,
    leaf: <path d="M11 20A7 7 0 0 1 4 13c0-7 8-11 16-11 0 8-4 16-11 16-3 0-4-3-4-3m0 0c0-5 4-9 8-10"/>,
    at: <><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1A10 10 0 1 0 18 20"/></>,
    database: <><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"/></>,
    globe: <><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18"/></>,
    list: <><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></>,
    filter: <path d="M3 4h18l-7 9v6l-4 2v-8z"/>,
    webhook: <><circle cx="12" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="18" r="3"/><path d="m9 8-3 7M15 8l3 7M9 18h6"/></>,
    key: <><circle cx="7.5" cy="15.5" r="4.5"/><path d="m11 12 9-9 3 3-3 3 2 2-2 2-2-2-3 3"/></>,
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round">
      {paths[name]}
    </svg>
  );
};

function Topbar({ onSearch, theme, setTheme, section, onMenuToggle }) {
  const sections = [
    { id: 'docs', label: 'Documentation', href: 'index.html' },
    { id: 'api', label: 'API Reference', href: 'api.html' },
    { id: 'benchmark', label: 'Benchmark', href: 'longmemeval.html' },
    { id: 'github', label: 'GitHub', href: 'https://github.com/carrickcheah/memgc' },
  ];
  return (
    <header className="topbar">
      <button
        className="menu-btn"
        onClick={onMenuToggle}
        aria-label="Open menu"
      >
        <Icon name="menu" size={20} stroke={2} />
      </button>
      <a className="brand" href="index.html">
        <div className="brand-mark">G</div>
        <span>MemGC</span>
        <span className="brand-tag">v0.0.2</span>
      </a>
      <nav className="topnav">
        {sections.map(s => (
          <a
            key={s.id}
            href={s.href}
            className={section === s.id ? 'active' : ''}
            target={s.href.startsWith('http') ? '_blank' : undefined}
            rel={s.href.startsWith('http') ? 'noreferrer' : undefined}
          >
            {s.label}
          </a>
        ))}
      </nav>
      <div className="topbar-right">
        <button className="search-trigger" onClick={onSearch}>
          <Icon name="search" size={14} />
          <span>Search documentation…</span>
          <span className="kbd">⌘K</span>
        </button>
        <button className="icon-btn" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} title="Toggle theme">
          <Icon name={theme === 'dark' ? 'sun' : 'moon'} size={16} />
        </button>
      </div>
    </header>
  );
}

const NAV = [
  {
    title: 'Getting started',
    items: [
      { label: 'Introduction', href: 'index.html', id: 'introduction' },
      { label: 'Quickstart', href: 'quickstart.html', id: 'quickstart' },
    ]
  },
  {
    title: 'Concepts',
    items: [
      { label: 'How it works', href: 'index.html#how', id: 'how' },
      { label: 'Calibrated decay', href: 'index.html#decay', id: 'decay' },
      { label: 'Hybrid recall', href: 'index.html#hybrid', id: 'hybrid' },
      { label: 'Audit & lineage', href: 'audit.html', id: 'audit', badge: 'new' },
      { label: 'Injection safety', href: 'index.html#injection', id: 'injection' },
      { label: 'Storage backends', href: 'index.html#storage', id: 'storage' },
    ]
  },
  {
    title: 'Reference',
    items: [
      { label: 'Public API', href: 'api.html', id: 'api' },
      { label: 'MemGC handle', href: 'api.html#memgc', id: 'api-memgc' },
      { label: 'Memory', href: 'api.html#memory', id: 'api-memory' },
      { label: 'Storage trait', href: 'api.html#storage', id: 'api-storage' },
      { label: 'Embedder trait', href: 'api.html#embedder', id: 'api-embedder' },
    ]
  },
  {
    title: 'Validation',
    items: [
      { label: 'LongMemEval R@5 = 0.984', href: 'longmemeval.html', id: 'longmemeval', badge: 'new' },
    ]
  },
  {
    title: 'Project',
    items: [
      { label: 'License (Apache 2.0)', href: 'license.html', id: 'license' },
      { label: 'GitHub', href: 'https://github.com/carrickcheah/memgc', id: 'github' },
      { label: 'PyPI', href: 'https://pypi.org/project/memgc/', id: 'pypi' },
    ]
  },
];

function Sidebar({ activeId, mobileOpen, onMobileClose }) {
  const [collapsed, setCollapsed] = React.useState({});
  const toggle = (t) => setCollapsed(s => ({ ...s, [t]: !s[t] }));
  return (
    <>
      {mobileOpen && <div className="sidebar-backdrop" onClick={onMobileClose} />}
      <aside className={`sidebar ${mobileOpen ? 'mobile-open' : ''}`}>
        <button className="sidebar-close" onClick={onMobileClose} aria-label="Close menu">
          <Icon name="x" size={18} stroke={2} />
        </button>
        {NAV.map((section, idx) => (
          <div key={idx} className={`side-section ${collapsed[section.title] ? 'collapsed' : ''}`}>
            <button className="side-section-header" onClick={() => toggle(section.title)}>
              {section.title}
              <span className="chev"><Icon name="chev" size={12} stroke={2} /></span>
            </button>
            <div className="side-items">
              {section.items.map(it => (
                <a key={it.id} href={it.href}
                   className={`side-link ${activeId === it.id ? 'active' : ''}`}
                   onClick={onMobileClose}>
                  {it.label}
                  {it.badge && <span className={`badge ${it.badge}`}>{it.badge}</span>}
                </a>
              ))}
            </div>
          </div>
        ))}
      </aside>
    </>
  );
}

function TOC({ items }) {
  const [active, setActive] = React.useState(items[0]?.id);
  React.useEffect(() => {
    const handler = () => {
      let current = items[0]?.id;
      for (const it of items) {
        const el = document.getElementById(it.id);
        if (el && el.getBoundingClientRect().top < 120) current = it.id;
      }
      setActive(current);
    };
    handler();
    window.addEventListener('scroll', handler, { passive: true });
    return () => window.removeEventListener('scroll', handler);
  }, [items]);
  return (
    <aside className="toc">
      <div className="toc-label">On this page</div>
      <ul>
        {items.map(it => (
          <li key={it.id} className={`lvl-${it.level || 2}`}>
            <a href={`#${it.id}`} className={active === it.id ? 'active' : ''}>{it.label}</a>
          </li>
        ))}
      </ul>
      <div className="toc-foot">
        <a href="#" onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }); }}>
          <Icon name="arrowUp" size={12} /> Back to top
        </a>
      </div>
    </aside>
  );
}

function CodeBlock({ tabs, filename, defaultTab = 0, pretty, code, raw, label }) {
  // Backward-compat: older invocations pass `pretty` (plain text) +
  // `code` (HTML-tinted) instead of `tabs`. Synthesize a single tab so
  // the rest of the component renders the same way. Without this the
  // component throws `Cannot read properties of undefined (reading
  // '0')` and the whole guide page errors out.
  const resolvedTabs = tabs ?? [
    {
      label: label ?? filename ?? "code",
      code: code ?? pretty ?? "",
      raw: raw ?? pretty,
    },
  ];
  const [active, setActive] = React.useState(defaultTab);
  const [copied, setCopied] = React.useState(false);
  const current = resolvedTabs[active];
  const copy = () => {
    const text = current.raw || current.code.replace(/<[^>]+>/g, '');
    navigator.clipboard?.writeText(text);
    setCopied(true);
    setTimeout(() => setCopied(false), 1400);
  };
  return (
    <div className="code-block">
      <div className="code-head">
        {resolvedTabs.length > 1 ? (
          <div className="code-tabs">
            {resolvedTabs.map((t, i) => (
              <button key={i} className={`code-tab ${i === active ? 'active' : ''}`} onClick={() => setActive(i)}>
                {t.label}
              </button>
            ))}
          </div>
        ) : (
          <span className="code-filename">{filename || resolvedTabs[0].label}</span>
        )}
        <div className="code-actions">
          <button className={`code-icon-btn ${copied ? 'copied' : ''}`} onClick={copy} title="Copy">
            <Icon name={copied ? 'check' : 'copy'} size={14} />
          </button>
        </div>
      </div>
      <pre className="code-body" dangerouslySetInnerHTML={{ __html: current.code }} />
    </div>
  );
}

// Dot — render Graphviz DOT source to inline SVG.
// Lazily loads viz-js from a CDN on first mount. If rendering fails
// (offline preview, CSP, blocked CDN), shows the DOT source as a code
// block so readers always see something. A rejected load is not cached,
// so a transient network blip doesn't permanently disable diagrams.
let vizLoadingPromise = null;
function loadViz() {
  if (window.Viz) return Promise.resolve(window.Viz);
  if (vizLoadingPromise) return vizLoadingPromise;
  vizLoadingPromise = new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = 'https://unpkg.com/@viz-js/viz@3.2.4/lib/viz-standalone.js';
    s.onload = () => resolve(window.Viz);
    s.onerror = () => {
      vizLoadingPromise = null; // clear on failure so next mount can retry
      reject(new Error('viz-js load failed'));
    };
    document.head.appendChild(s);
  });
  return vizLoadingPromise;
}

function Dot({ src, caption, maxWidth = 780, ariaLabel }) {
  const hostRef = React.useRef(null);
  const [state, setState] = React.useState('loading');

  React.useEffect(() => {
    let cancelled = false;

    (async () => {
      try {
        await loadViz();
        const viz = await window.Viz.instance();
        if (cancelled) return;
        const svg = viz.renderSVGElement(src);
        svg.removeAttribute('width');
        svg.removeAttribute('height');
        svg.style.maxWidth = '100%';
        svg.style.height = 'auto';
        const host = hostRef.current;
        if (host) {
          while (host.firstChild) host.removeChild(host.firstChild);
          host.appendChild(svg);
          setState('ready');
        }
      } catch (_) {
        if (!cancelled) setState('error');
      }
    })();

    return () => { cancelled = true; };
  }, [src]);

  // Keep the host div (which we mutate imperatively) separate from any
  // React-managed sibling. Sharing a parent would cause React's reconciler
  // to trip over nodes we already removed via appendChild/removeChild.
  return (
    <figure role="img" aria-label={ariaLabel || caption || 'diagram'}
      style={{ margin: '20px 0', padding: '18px 20px', border: '1px solid var(--border)', borderRadius: 8, background: 'var(--surface)' }}>
      <div style={{ maxWidth, margin: '0 auto', overflow: 'auto', textAlign: 'center' }}>
        <div ref={hostRef} style={{ display: state === 'ready' ? 'block' : 'none' }} />
        {state === 'loading' && (
          <div style={{ color: 'var(--muted)', fontSize: 13, padding: '40px 0' }}>rendering diagram…</div>
        )}
        {state === 'error' && (
          <pre className="codeblock" style={{ fontSize: 12, textAlign: 'left', lineHeight: 1.55, padding: 14 }}>{src}</pre>
        )}
      </div>
      {caption && (
        <figcaption style={{ fontSize: 13, color: 'var(--muted)', marginTop: 10, textAlign: 'center' }}>{caption}</figcaption>
      )}
    </figure>
  );
}

function Callout({ type = 'info', title, children }) {
  const icons = { info: 'info', warn: 'warn', note: 'sparkle', danger: 'warn' };
  return (
    <div className={`callout ${type}`}>
      <div className="ico"><Icon name={icons[type]} size={14} /></div>
      <div>
        {title && <div className="callout-title">{title}</div>}
        <div>{children}</div>
      </div>
    </div>
  );
}

function Feedback() {
  const [rating, setRating] = React.useState(null);
  const [sent, setSent] = React.useState(false);
  if (sent) {
    return (
      <div className="feedback">
        <div>
          <h4>Thanks for the feedback.</h4>
          <p>We read every note and use it to improve these docs.</p>
        </div>
        <div>
          <div className="card-ico" style={{marginBottom:0, background:'var(--accent-soft)', color:'var(--accent-ink)'}}>
            <Icon name="check" size={16} stroke={2.2} />
          </div>
        </div>
      </div>
    );
  }
  return (
    <div className={`feedback ${rating !== null ? 'open' : ''}`}>
      <div>
        <h4>Was this page helpful?</h4>
        <p>Your feedback helps us sharpen the docs.</p>
      </div>
      <div className="fb-actions">
        <button className={`fb-btn ${rating === 'up' ? 'selected' : ''}`} onClick={() => setRating('up')}>
          <Icon name="thumbUp" size={14} /> Yes
        </button>
        <button className={`fb-btn ${rating === 'down' ? 'selected' : ''}`} onClick={() => setRating('down')}>
          <Icon name="thumbDown" size={14} /> No
        </button>
      </div>
      <div className="feedback-form">
        <textarea placeholder={rating === 'up' ? 'What worked well?' : 'What could be better? Be specific — we read everything.'} />
        <div className="row">
          <button className="btn-ghost" onClick={() => setRating(null)}>Cancel</button>
          <button className="btn-primary" onClick={() => setSent(true)}>Send feedback</button>
        </div>
      </div>
    </div>
  );
}

function PageFoot({ prev, next }) {
  return (
    <div className="page-foot">
      {prev ? (
        <a href={prev.href}>
          <div className="label">← Previous</div>
          <div className="title"><Icon name="arrowL" size={12} /> {prev.label}</div>
        </a>
      ) : <div />}
      {next ? (
        <a href={next.href} className="next">
          <div className="label">Next →</div>
          <div className="title">{next.label} <Icon name="arrowR" size={12} /></div>
        </a>
      ) : <div />}
    </div>
  );
}

const SEARCH_INDEX = [
  // Getting started
  { group: 'Getting started', title: 'Introduction — self-hostable AI memory for production agents', path: '/', href: 'index.html', icon: 'book' },
  { group: 'Getting started', title: 'Quickstart — uv add memgc and your first call', path: '/quickstart', href: 'quickstart.html', icon: 'rocket' },

  // Concepts
  { group: 'Concepts', title: 'How it works — extract, retrieve, consolidate, sweep', path: '/concepts/how', href: 'index.html#how', icon: 'puzzle' },
  { group: 'Concepts', title: 'Calibrated decay — 6-component score + half-life', path: '/concepts/decay', href: 'index.html#decay', icon: 'cpu' },
  { group: 'Concepts', title: 'Hybrid recall — vector + BM25 via tokio::join', path: '/concepts/hybrid', href: 'index.html#hybrid', icon: 'zap' },
  { group: 'Concepts', title: 'Audit & lineage — version chain + persistent SQL log', path: '/concepts/audit', href: 'audit.html', icon: 'shield' },
  { group: 'Concepts', title: 'Injection safety — six prompt-injection defenses', path: '/concepts/injection', href: 'index.html#injection', icon: 'shield' },
  { group: 'Concepts', title: 'Storage backends — SQLite, Postgres, in-memory', path: '/concepts/storage', href: 'index.html#storage', icon: 'database' },

  // Reference
  { group: 'Reference', title: 'Public API overview', path: '/api', href: 'api.html', icon: 'code' },
  { group: 'Reference', title: 'MemGC handle — extract, retrieve, consolidate, sweep', path: '/api/memgc', href: 'api.html#memgc', icon: 'code' },
  { group: 'Reference', title: 'Memory — id, scope, content, tags, embedding', path: '/api/memory', href: 'api.html#memory', icon: 'database' },
  { group: 'Reference', title: 'Storage trait — SQLite, Postgres, InMemory', path: '/api/storage', href: 'api.html#storage', icon: 'database' },
  { group: 'Reference', title: 'Embedder trait — OpenAI, Azure, Voyage, Mock', path: '/api/embedder', href: 'api.html#embedder', icon: 'cpu' },
  { group: 'Reference', title: 'AuditEntry — op, scope_or_id, at, duration_ms, ok', path: '/api/audit', href: 'audit.html#api', icon: 'list' },

  // Validation
  { group: 'Validation', title: 'LongMemEval R@5 = 0.984 on 450-question held-out', path: '/longmemeval', href: 'longmemeval.html', icon: 'sparkle' },
  { group: 'Validation', title: 'Benchmark integrity disclosure — what we tuned, what we measured', path: '/longmemeval#integrity', href: 'longmemeval.html#integrity', icon: 'shield' },

  // Project
  { group: 'Project', title: 'License — Apache 2.0 (with patent grant)', path: '/license', href: 'license.html', icon: 'leaf' },
  { group: 'Project', title: 'GitHub — github.com/carrickcheah/memgc', path: '/github', href: 'https://github.com/carrickcheah/memgc', icon: 'ext' },
  { group: 'Project', title: 'PyPI — pypi.org/project/memgc', path: '/pypi', href: 'https://pypi.org/project/memgc/', icon: 'ext' },
];

function SearchOverlay({ open, onClose }) {
  const [q, setQ] = React.useState('');
  const [focus, setFocus] = React.useState(0);
  const inputRef = React.useRef();
  React.useEffect(() => {
    if (open) {
      setTimeout(() => inputRef.current?.focus(), 20);
      setQ(''); setFocus(0);
    }
  }, [open]);
  const results = React.useMemo(() => {
    if (!q.trim()) return SEARCH_INDEX;
    const needle = q.toLowerCase();
    return SEARCH_INDEX.filter(r =>
      r.title.toLowerCase().includes(needle) ||
      r.path.toLowerCase().includes(needle) ||
      r.group.toLowerCase().includes(needle)
    );
  }, [q]);
  const grouped = React.useMemo(() => {
    const g = {};
    results.forEach(r => { (g[r.group] = g[r.group] || []).push(r); });
    return g;
  }, [results]);
  const flat = results;

  React.useEffect(() => {
    if (!open) return;
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowDown') { e.preventDefault(); setFocus(f => Math.min(flat.length - 1, f + 1)); }
      if (e.key === 'ArrowUp') { e.preventDefault(); setFocus(f => Math.max(0, f - 1)); }
      if (e.key === 'Enter' && flat[focus]) {
        window.location.href = flat[focus].href;
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open, flat, focus, onClose]);

  const highlight = (text) => {
    if (!q.trim()) return text;
    const i = text.toLowerCase().indexOf(q.toLowerCase());
    if (i < 0) return text;
    return <>{text.slice(0,i)}<mark>{text.slice(i, i+q.length)}</mark>{text.slice(i+q.length)}</>;
  };

  let counter = 0;
  return (
    <div className={`search-overlay ${open ? 'open' : ''}`} onMouseDown={(e) => e.target === e.currentTarget && onClose()}>
      <div className="search-modal" onMouseDown={e => e.stopPropagation()}>
        <div className="search-input-wrap">
          <Icon name="search" size={16} />
          <input ref={inputRef} className="search-input" placeholder="Search for anything…"
                 value={q} onChange={e => { setQ(e.target.value); setFocus(0); }} />
          <button className="icon-btn" onClick={onClose} style={{width: 28, height: 28}}>
            <span className="kbd" style={{fontSize:10}}>ESC</span>
          </button>
        </div>
        <div className="search-results">
          {flat.length === 0 ? (
            <div className="search-empty">No results for “{q}”. Try a different term.</div>
          ) : Object.entries(grouped).map(([group, rows]) => (
            <div key={group}>
              <div className="search-group-label">{group}</div>
              {rows.map((r) => {
                const idx = counter++;
                return (
                  <a key={r.title} href={r.href}
                     className={`search-result ${idx === focus ? 'focused' : ''}`}
                     onMouseEnter={() => setFocus(idx)}>
                    <span className="res-ico"><Icon name={r.icon} size={14} /></span>
                    <span className="res-title">{highlight(r.title)}</span>
                    <span className="res-path">{r.path}</span>
                  </a>
                );
              })}
            </div>
          ))}
        </div>
        <div className="search-foot">
          <div className="item"><span className="kbd">↑</span><span className="kbd">↓</span> navigate</div>
          <div className="item"><span className="kbd">↵</span> select</div>
          <div className="item"><span className="kbd">ESC</span> close</div>
          <div className="item" style={{marginLeft:'auto'}}>Powered by local index</div>
        </div>
      </div>
    </div>
  );
}

function TweaksPanel({ visible, theme, setTheme }) {
  if (!visible) return null;
  return (
    <div className="tweaks-panel visible">
      <div className="tweaks-title">Tweaks</div>
      <div className="tweaks-row">
        <span>Theme</span>
        <div className="toggle-group">
          <button className={`toggle-btn ${theme === 'light' ? 'active' : ''}`} onClick={() => setTheme('light')}>Light</button>
          <button className={`toggle-btn ${theme === 'dark' ? 'active' : ''}`} onClick={() => setTheme('dark')}>Dark</button>
        </div>
      </div>
    </div>
  );
}

function useShell(defaultTheme = 'light') {
  const [theme, setThemeState] = React.useState(() => localStorage.getItem('memgc-theme') || defaultTheme);
  const [searchOpen, setSearchOpen] = React.useState(false);
  const [tweaksVisible, setTweaksVisible] = React.useState(false);
  const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);

  const setTheme = React.useCallback((t) => {
    setThemeState(t);
    localStorage.setItem('memgc-theme', t);
    document.documentElement.setAttribute('data-theme', t);
  }, []);

  React.useEffect(() => {
    document.documentElement.setAttribute('data-theme', theme);
  }, [theme]);

  React.useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
        e.preventDefault();
        setSearchOpen(s => !s);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  // Lock body scroll when mobile drawer is open
  React.useEffect(() => {
    document.body.style.overflow = mobileMenuOpen ? 'hidden' : '';
    return () => { document.body.style.overflow = ''; };
  }, [mobileMenuOpen]);

  return {
    theme, setTheme,
    searchOpen, setSearchOpen,
    tweaksVisible,
    mobileMenuOpen, setMobileMenuOpen,
  };
}

Object.assign(window, {
  Icon, Topbar, Sidebar, TOC, CodeBlock, Callout, Dot, Feedback, PageFoot,
  SearchOverlay, TweaksPanel, useShell, NAV, SEARCH_INDEX,
});
