"""
Interactive HTML viewer — generates a self-contained HTML file with
pan/zoom/search diagrams and a styled HLD report.

Outputs:
  hld_viewer.html — interactive single-page viewer with:
    - Collapsible sidebar with dark mode toggle
    - Progressive disclosure (expandable cards, accordions, show-more)
    - Tabbed Report / Diagram views with ARIA accessibility
    - Package-level (collapsed) and file-level (detailed) diagram toggle
    - SVG pan/zoom via svg-pan-zoom
    - Node search with highlight and keyboard shortcuts
    - Responsive design (mobile bottom-bar)
    - Smooth micro-interactions (150-250ms)
"""

from __future__ import annotations

import html
import logging
from datetime import datetime, timezone
from pathlib import Path

from hld_generator import __version__
from hld_generator.fallback import FallbackAnalyzer
from hld_generator.graph import CodeGraph
from hld_generator.llm import LLMAnalysis

logger = logging.getLogger(__name__)


def build_package_diagram(cg: CodeGraph) -> str:
    """Collapse file-level nodes into package nodes with deduped edges."""
    mod_to_pkg: dict[str, str] = {}
    for pkg, members in cg.packages.items():
        for mid in members:
            mod_to_pkg[mid] = pkg

    pkg_edges: set[tuple[str, str]] = set()
    for src, dst in cg.graph.edges:
        src_pkg = mod_to_pkg.get(src)
        dst_pkg = mod_to_pkg.get(dst)
        if src_pkg and dst_pkg and src_pkg != dst_pkg:
            pkg_edges.add((src_pkg, dst_pkg))

    lines = ["graph TD"]
    for pkg in sorted(cg.packages):
        sid = FallbackAnalyzer.safe_mermaid_id(pkg)
        count = len(cg.packages[pkg])
        label = FallbackAnalyzer.safe_mermaid_label(f"{pkg} ({count} files)")
        lines.append(f'    {sid}["{label}"]')

    for src_pkg, dst_pkg in sorted(pkg_edges):
        s = FallbackAnalyzer.safe_mermaid_id(src_pkg)
        d = FallbackAnalyzer.safe_mermaid_id(dst_pkg)
        lines.append(f"    {s} --> {d}")

    return "\n".join(lines)


def _esc(text: str) -> str:
    """HTML-escape a string."""
    return html.escape(str(text), quote=True)


def _escape_for_js(text: str) -> str:
    """Escape a string for safe embedding inside a JS template literal (backticks)."""
    return text.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")


class HTMLRenderer:
    """Generate a self-contained interactive HTML viewer."""

    def __init__(self, output_dir: Path) -> None:
        self.output_dir = output_dir
        self.output_dir.mkdir(parents=True, exist_ok=True)

    def render(self, analysis: LLMAnalysis, code_graph: CodeGraph) -> list[Path]:
        """Generate the interactive HTML file and return its path."""
        path = self.output_dir / "hld_viewer.html"

        mermaid_detailed = analysis.mermaid_diagram or ""
        mermaid_package = build_package_diagram(code_graph)
        report_html = self._render_report_html(analysis, code_graph)

        content = self._build_html(
            project_name=analysis.project_name or "Codebase Analysis",
            mermaid_detailed=mermaid_detailed,
            mermaid_package=mermaid_package,
            report_html=report_html,
        )

        path.write_text(content, encoding="utf-8")
        logger.info("Wrote HTML viewer: %s", path)
        return [path]

    # ── Package-level diagram ──────────────────────────────────────────────

    @staticmethod
    def _build_package_diagram(cg: CodeGraph) -> str:
        """Collapse file-level nodes into package nodes with deduped edges."""
        return build_package_diagram(cg)

    # ── Report HTML ────────────────────────────────────────────────────────

    def _render_report_html(self, analysis: LLMAnalysis, cg: CodeGraph) -> str:
        """Convert analysis into styled HTML sections with progressive disclosure."""
        s: list[str] = []
        now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")

        # Header
        s.append('<div class="report-header">')
        s.append(f'<p class="meta">Generated on {now} by HLD Generator v{__version__}</p>')
        s.append(f'<p class="summary">{_esc(analysis.summary)}</p>')
        if analysis.architecture_pattern:
            s.append(f'<span class="arch-badge">{_esc(analysis.architecture_pattern)}</span>')
        s.append('</div>')

        # Stat cards
        stats = [
            ("Modules", str(len(cg.modules)), '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>'),
            ("Lines of Code", f"{cg.total_lines:,}", '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>'),
            ("Packages", str(len(cg.packages)), '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>'),
            ("Dependencies", str(cg.graph.number_of_edges()), '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>'),
            ("Languages", str(len(cg.languages)), '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>'),
        ]
        s.append('<div class="stat-grid">')
        for label, value, icon in stats:
            s.append('<div class="stat-card">')
            s.append(f'<div class="stat-card__icon">{icon}</div>')
            s.append(f'<div class="stat-card__value">{_esc(value)}</div>')
            s.append(f'<div class="stat-card__label">{_esc(label)}</div>')
            s.append('</div>')
        s.append('</div>')

        # Tech Stack
        if analysis.tech_stack:
            s.append('<div class="section">')
            s.append('<h2 class="section__title">Technology Stack</h2>')
            s.append('<div class="badges">')
            ts = analysis.tech_stack
            for category in ("languages", "frameworks", "databases", "infrastructure"):
                for item in ts.get(category, []):
                    s.append(f'<span class="badge badge--{category}">{_esc(item)}</span>')
            s.append("</div></div>")

        # Components — accordion
        if analysis.components:
            count = len(analysis.components)
            s.append('<div class="section">')
            s.append(f'<h2 class="section__title">Components <span class="count-badge">{count}</span></h2>')
            s.append('<div class="accordion" role="list">')
            for i, comp in enumerate(analysis.components):
                cid = f"comp-{i}"
                name = _esc(comp.get("name", "Unknown"))
                layer = _esc(comp.get("layer", "N/A"))
                purpose = _esc(comp.get("purpose", ""))
                modules = comp.get("key_modules", [])
                layer_cls = layer.lower().replace(" ", "-").replace("/", "-")
                s.append('<div class="expandable-card" role="listitem">')
                s.append(f'<button class="expandable-card__trigger" '
                         f'aria-expanded="false" aria-controls="{cid}" '
                         f'tabindex="0">')
                s.append('<div class="expandable-card__header">')
                s.append(f'<span class="expandable-card__title">{name}</span>')
                s.append(f'<span class="badge badge--layer badge--{layer_cls}">{layer}</span>')
                s.append('</div>')
                s.append('<svg class="expandable-card__chevron" viewBox="0 0 24 24" '
                         'fill="none" stroke="currentColor" stroke-width="2" '
                         'width="16" height="16"><polyline points="6 9 12 15 18 9"/></svg>')
                s.append('</button>')
                s.append(f'<div class="expandable-card__content" id="{cid}" '
                         f'role="region" aria-hidden="true">')
                s.append('<div class="expandable-card__inner">')
                if purpose:
                    s.append(f"<p>{purpose}</p>")
                if modules:
                    s.append("<ul>")
                    for m in modules[:10]:
                        s.append(f"<li><code>{_esc(m)}</code></li>")
                    if len(modules) > 10:
                        s.append(f'<li class="text-muted">...and {len(modules) - 10} more</li>')
                    s.append("</ul>")
                s.append('</div></div></div>')
            s.append('</div></div>')

        # Data Flow — collapsible section
        if analysis.data_flow:
            count = len(analysis.data_flow)
            s.append('<div class="section">')
            s.append('<button class="section__toggle" aria-expanded="false" '
                     'aria-controls="data-flow-content">')
            s.append(f'<h2 class="section__title">Data Flow <span class="count-badge">{count}</span></h2>')
            s.append('<svg class="section__chevron" viewBox="0 0 24 24" '
                     'fill="none" stroke="currentColor" stroke-width="2" '
                     'width="16" height="16"><polyline points="6 9 12 15 18 9"/></svg>')
            s.append('</button>')
            s.append('<div class="section__content" id="data-flow-content" aria-hidden="true">')
            s.append('<table><thead><tr><th>From</th><th>To</th><th>Description</th></tr></thead><tbody>')
            for flow in analysis.data_flow[:30]:
                src = _esc(flow.get("from", ""))
                dst = _esc(flow.get("to", ""))
                desc = _esc(flow.get("description", ""))
                s.append(f"<tr><td><code>{src}</code></td><td><code>{dst}</code></td><td>{desc}</td></tr>")
            s.append("</tbody></table></div></div>")

        # Entry Points — collapsible
        if cg.entry_points:
            count = len(cg.entry_points)
            s.append('<div class="section">')
            s.append('<button class="section__toggle" aria-expanded="false" '
                     'aria-controls="entry-points-content">')
            s.append(f'<h2 class="section__title">Entry Points <span class="count-badge">{count}</span></h2>')
            s.append('<svg class="section__chevron" viewBox="0 0 24 24" '
                     'fill="none" stroke="currentColor" stroke-width="2" '
                     'width="16" height="16"><polyline points="6 9 12 15 18 9"/></svg>')
            s.append('</button>')
            s.append('<div class="section__content" id="entry-points-content" aria-hidden="true">')
            visible = cg.entry_points[:5]
            hidden = cg.entry_points[5:20]
            s.append("<ul>")
            for ep in visible:
                s.append(f"<li><code>{_esc(ep)}</code></li>")
            s.append("</ul>")
            if hidden:
                s.append('<div class="show-more" data-visible="0">')
                s.append("<ul>")
                for ep in hidden:
                    s.append(f'<li class="show-more__item" style="display:none"><code>{_esc(ep)}</code></li>')
                s.append("</ul>")
                s.append(f'<button class="show-more__toggle">Show {len(hidden)} more</button>')
                s.append('</div>')
            s.append("</div></div>")

        # API Endpoints — collapsible
        all_endpoints = [
            (mid, ep)
            for mid, mod in cg.modules.items()
            for ep in mod.endpoints
        ]
        if all_endpoints:
            count = len(all_endpoints)
            s.append('<div class="section">')
            s.append('<button class="section__toggle" aria-expanded="false" '
                     'aria-controls="api-endpoints-content">')
            s.append(f'<h2 class="section__title">API Endpoints <span class="count-badge">{count}</span></h2>')
            s.append('<svg class="section__chevron" viewBox="0 0 24 24" '
                     'fill="none" stroke="currentColor" stroke-width="2" '
                     'width="16" height="16"><polyline points="6 9 12 15 18 9"/></svg>')
            s.append('</button>')
            s.append('<div class="section__content" id="api-endpoints-content" aria-hidden="true">')
            s.append('<table><thead><tr><th>Route</th><th>Module</th></tr></thead><tbody>')
            for mid, ep in all_endpoints[:50]:
                s.append(f"<tr><td><code>{_esc(ep)}</code></td><td><code>{_esc(mid)}</code></td></tr>")
            s.append("</tbody></table></div></div>")

        # Observations — collapsible
        if analysis.observations:
            count = len(analysis.observations)
            s.append('<div class="section">')
            s.append('<button class="section__toggle" aria-expanded="false" '
                     'aria-controls="observations-content">')
            s.append(f'<h2 class="section__title">Observations <span class="count-badge">{count}</span></h2>')
            s.append('<svg class="section__chevron" viewBox="0 0 24 24" '
                     'fill="none" stroke="currentColor" stroke-width="2" '
                     'width="16" height="16"><polyline points="6 9 12 15 18 9"/></svg>')
            s.append('</button>')
            s.append('<div class="section__content" id="observations-content" aria-hidden="true">')
            s.append("<ul>")
            for obs in analysis.observations:
                s.append(f"<li>{_esc(obs)}</li>")
            s.append("</ul></div></div>")

        if analysis.error:
            s.append(f'<div class="warning">'
                     f'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '
                     f'width="16" height="16"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94'
                     f'a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>'
                     f'<line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
                     f' LLM Analysis Note: {_esc(analysis.error)}</div>')

        return "\n".join(s)

    # ── HTML assembly ──────────────────────────────────────────────────────

    def _build_html(
        self,
        project_name: str,
        mermaid_detailed: str,
        mermaid_package: str,
        report_html: str,
    ) -> str:
        """Assemble the full self-contained HTML document."""
        return _HTML_TEMPLATE.format(
            title=_esc(project_name),
            report_html=report_html,
            mermaid_detailed=_escape_for_js(mermaid_detailed),
            mermaid_package=_escape_for_js(mermaid_package),
        )


# ── HTML Template ──────────────────────────────────────────────────────────
# Single self-contained HTML page with embedded CSS/JS.
# Mermaid and svg-pan-zoom loaded from CDN.

_HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title} — HLD Viewer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ── Design Tokens ── */
:root {{
  /* Spacing (4px base grid) */
  --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
  --space-5: 20px; --space-6: 24px; --space-8: 32px; --space-10: 40px;
  --space-12: 48px; --space-16: 64px;

  /* Typography */
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-mono: 'SF Mono', 'Fira Code', Consolas, monospace;
  --text-xs: 0.75rem; --text-sm: 0.8125rem; --text-base: 0.875rem;
  --text-md: 1rem; --text-lg: 1.125rem; --text-xl: 1.25rem;
  --text-2xl: 1.5rem; --text-3xl: 1.875rem;
  --leading-tight: 1.25; --leading-normal: 1.5; --leading-relaxed: 1.7;
  --tracking-tight: -0.02em;
  --weight-normal: 400; --weight-medium: 500; --weight-semibold: 600; --weight-bold: 700;

  /* Radius */
  --radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px; --radius-xl: 12px; --radius-full: 9999px;

  /* Shadows */
  --shadow-xs: 0 1px 2px rgba(0,0,0,0.05);
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.07), 0 2px 4px rgba(0,0,0,0.04);
  --shadow-lg: 0 10px 15px rgba(0,0,0,0.07), 0 4px 6px rgba(0,0,0,0.04);

  /* Transitions */
  --transition-fast: 150ms ease;
  --transition-base: 200ms ease;
  --transition-slow: 300ms ease;
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);

  /* Colors — Light */
  --color-bg: #ffffff;
  --color-bg-subtle: #f8fafc;
  --color-bg-muted: #f1f5f9;
  --color-bg-elevated: #ffffff;
  --color-surface: #ffffff;
  --color-sidebar-bg: #0f172a;
  --color-sidebar-text: #e2e8f0;
  --color-sidebar-muted: #64748b;
  --color-sidebar-hover: #1e293b;
  --color-sidebar-active: #3b82f6;
  --color-text-primary: #0f172a;
  --color-text-secondary: #475569;
  --color-text-muted: #94a3b8;
  --color-text-inverse: #ffffff;
  --color-border: #e2e8f0;
  --color-border-muted: #f1f5f9;
  --color-accent: #3b82f6;
  --color-accent-hover: #2563eb;
  --color-accent-subtle: #eff6ff;
  --color-warning-bg: #fffbeb;
  --color-warning-border: #f59e0b;
  --color-warning-text: #92400e;

  /* Badge colors */
  --color-badge-lang-bg: #dbeafe; --color-badge-lang-text: #1e40af;
  --color-badge-fw-bg: #dcfce7; --color-badge-fw-text: #166534;
  --color-badge-db-bg: #fef3c7; --color-badge-db-text: #92400e;
  --color-badge-infra-bg: #f3e8ff; --color-badge-infra-text: #6b21a8;
}}

/* ── Dark Mode ── */
[data-theme="dark"] {{
  --color-bg: #09090b;
  --color-bg-subtle: #111113;
  --color-bg-muted: #1a1a1e;
  --color-bg-elevated: #161618;
  --color-surface: #161618;
  --color-sidebar-bg: #09090b;
  --color-text-primary: #fafafa;
  --color-text-secondary: #a1a1aa;
  --color-text-muted: #71717a;
  --color-text-inverse: #09090b;
  --color-border: #27272a;
  --color-border-muted: #1e1e21;
  --color-accent: #3b82f6;
  --color-accent-hover: #60a5fa;
  --color-accent-subtle: #172554;
  --color-warning-bg: #451a03;
  --color-warning-border: #d97706;
  --color-warning-text: #fde68a;
  --color-badge-lang-bg: #1e3a5f; --color-badge-lang-text: #93c5fd;
  --color-badge-fw-bg: #14532d; --color-badge-fw-text: #86efac;
  --color-badge-db-bg: #451a03; --color-badge-db-text: #fde68a;
  --color-badge-infra-bg: #3b0764; --color-badge-infra-text: #d8b4fe;
  --shadow-xs: 0 1px 2px rgba(0,0,0,0.4);
  --shadow-sm: 0 1px 3px rgba(0,0,0,0.5);
  --shadow-md: 0 4px 6px rgba(0,0,0,0.5);
}}

/* ── Reduced motion ── */
@media (prefers-reduced-motion: reduce) {{
  *, *::before, *::after {{
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }}
}}

/* ── Reset ── */
*, *::before, *::after {{ margin: 0; padding: 0; box-sizing: border-box; }}

body {{
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  color: var(--color-text-primary);
  background: var(--color-bg);
  display: flex;
  height: 100vh;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}}

/* ── Skip link ── */
.skip-link {{
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--color-accent);
  color: white;
  padding: var(--space-2) var(--space-4);
  z-index: 1000;
  font-size: var(--text-sm);
  transition: top var(--transition-fast);
}}
.skip-link:focus {{
  top: 0;
}}

/* ── Sidebar ── */
.sidebar {{
  width: 260px;
  min-width: 260px;
  background: var(--color-sidebar-bg);
  color: var(--color-sidebar-text);
  display: flex;
  flex-direction: column;
  padding: var(--space-6) var(--space-4);
  transition: width var(--transition-base) var(--ease-out-expo),
              min-width var(--transition-base) var(--ease-out-expo);
  overflow: hidden;
}}
.sidebar[data-collapsed="true"] {{
  width: 56px;
  min-width: 56px;
  padding: var(--space-6) var(--space-2);
}}
.sidebar__header {{
  margin-bottom: var(--space-6);
  overflow: hidden;
  white-space: nowrap;
}}
.sidebar__title {{
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  overflow: hidden;
  text-overflow: ellipsis;
}}
.sidebar__version {{
  font-size: var(--text-xs);
  color: var(--color-sidebar-muted);
  margin-top: var(--space-1);
}}
.sidebar[data-collapsed="true"] .sidebar__header {{
  opacity: 0;
  height: 0;
  margin: 0;
}}

/* Nav tabs */
.sidebar__nav {{
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  flex: 1;
}}
.sidebar__tab {{
  background: transparent;
  border: none;
  color: var(--color-sidebar-muted);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  padding: var(--space-2) var(--space-3);
  text-align: left;
  border-radius: var(--radius-md);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  transition: background var(--transition-fast), color var(--transition-fast);
  white-space: nowrap;
  overflow: hidden;
}}
.sidebar__tab:hover {{ background: var(--color-sidebar-hover); color: var(--color-sidebar-text); }}
.sidebar__tab:focus-visible {{
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}}
.sidebar__tab[aria-selected="true"] {{
  background: var(--color-sidebar-active);
  color: var(--color-text-inverse);
  font-weight: var(--weight-semibold);
}}
.sidebar__tab-icon {{
  width: 18px;
  height: 18px;
  flex-shrink: 0;
}}
.sidebar__tab-icon svg {{ width: 18px; height: 18px; }}
.sidebar__tab-label {{
  overflow: hidden;
  text-overflow: ellipsis;
}}
.sidebar[data-collapsed="true"] .sidebar__tab-label {{ display: none; }}

/* Sidebar footer */
.sidebar__footer {{
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-4);
  border-top: 1px solid rgba(255,255,255,0.08);
  padding-top: var(--space-4);
}}
.sidebar__footer-btn {{
  background: transparent;
  border: none;
  color: var(--color-sidebar-muted);
  cursor: pointer;
  padding: var(--space-2);
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color var(--transition-fast), background var(--transition-fast);
}}
.sidebar__footer-btn:hover {{ color: var(--color-sidebar-text); background: var(--color-sidebar-hover); }}
.sidebar__footer-btn:focus-visible {{
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}}
.sidebar__footer-btn svg {{ width: 18px; height: 18px; }}
.sidebar[data-collapsed="true"] .sidebar__footer {{
  flex-direction: column;
  align-items: center;
}}

/* ── Main area ── */
main {{
  flex: 1;
  background: var(--color-bg);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}}
#main-content {{ flex: 1; display: flex; flex-direction: column; overflow: hidden; }}
.tab-panel {{ display: none; flex: 1; overflow-y: auto; opacity: 0; transition: opacity var(--transition-fast); }}
.tab-panel.active {{ display: flex; flex-direction: column; opacity: 1; }}

/* ── Report ── */
.report {{
  padding: var(--space-10) var(--space-12);
  max-width: 960px;
  width: 100%;
}}
.report-header {{
  margin-bottom: var(--space-8);
  padding-bottom: var(--space-6);
  border-bottom: 1px solid var(--color-border);
}}
.report-header .meta {{
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-weight: var(--weight-medium);
}}
.report-header .summary {{
  color: var(--color-text-secondary);
  line-height: var(--leading-relaxed);
  margin: var(--space-3) 0;
  font-size: var(--text-md);
}}
.arch-badge {{
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  background: var(--color-accent-subtle);
  color: var(--color-accent);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}}

/* Stat grid */
.stat-grid {{
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-8);
}}
.stat-card {{
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  transition: box-shadow var(--transition-fast), border-color var(--transition-fast);
}}
.stat-card:hover {{
  box-shadow: var(--shadow-sm);
  border-color: var(--color-accent);
}}
.stat-card__icon {{
  color: var(--color-text-muted);
  margin-bottom: var(--space-3);
}}
.stat-card__icon svg {{ width: 20px; height: 20px; }}
.stat-card__value {{
  font-size: var(--text-2xl);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-tight);
  color: var(--color-text-primary);
  line-height: 1;
}}
.stat-card__label {{
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  margin-top: var(--space-1);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}}

/* Sections */
.section {{
  margin-bottom: var(--space-6);
}}
.section__title {{
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  color: var(--color-text-primary);
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}}
.section__toggle {{
  display: flex;
  align-items: center;
  width: 100%;
  background: none;
  border: none;
  cursor: pointer;
  padding: var(--space-3) 0;
  font-family: var(--font-sans);
  text-align: left;
  color: var(--color-text-primary);
  border-radius: var(--radius-md);
  transition: background var(--transition-fast);
}}
.section__toggle:hover {{
  background: var(--color-bg-subtle);
}}
.section__toggle:focus-visible {{
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}}
.section__toggle .section__title {{
  margin: 0;
  flex: 1;
}}
.section__chevron {{
  color: var(--color-text-muted);
  transition: transform var(--transition-fast);
  flex-shrink: 0;
  margin-left: var(--space-2);
}}
.section__toggle[aria-expanded="true"] .section__chevron {{
  transform: rotate(180deg);
}}
.section__content {{
  max-height: 0;
  overflow: hidden;
  transition: max-height var(--transition-base) var(--ease-out-expo);
}}

.count-badge {{
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 var(--space-2);
  border-radius: var(--radius-full);
  background: var(--color-bg-muted);
  color: var(--color-text-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}}

/* Badges */
.badges {{ display: flex; flex-wrap: wrap; gap: var(--space-2); margin: var(--space-2) 0; }}
.badge {{
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  transition: transform var(--transition-fast);
}}
.badge:hover {{ transform: translateY(-1px); }}
.badge--languages {{ background: var(--color-badge-lang-bg); color: var(--color-badge-lang-text); }}
.badge--frameworks {{ background: var(--color-badge-fw-bg); color: var(--color-badge-fw-text); }}
.badge--databases {{ background: var(--color-badge-db-bg); color: var(--color-badge-db-text); }}
.badge--infrastructure {{ background: var(--color-badge-infra-bg); color: var(--color-badge-infra-text); }}
.badge--layer {{
  font-size: 0.6875rem;
  padding: 2px var(--space-2);
  background: var(--color-bg-muted);
  color: var(--color-text-secondary);
}}

/* Expandable card */
.expandable-card {{
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-2);
  overflow: hidden;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}}
.expandable-card:hover {{
  border-color: var(--color-accent);
}}
.expandable-card__trigger {{
  display: flex;
  align-items: center;
  width: 100%;
  padding: var(--space-3) var(--space-4);
  background: none;
  border: none;
  cursor: pointer;
  font-family: var(--font-sans);
  text-align: left;
  color: var(--color-text-primary);
  transition: background var(--transition-fast);
}}
.expandable-card__trigger:hover {{
  background: var(--color-bg-subtle);
}}
.expandable-card__trigger:focus-visible {{
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}}
.expandable-card__header {{
  flex: 1;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}}
.expandable-card__title {{
  font-weight: var(--weight-semibold);
  font-size: var(--text-sm);
}}
.expandable-card__chevron {{
  color: var(--color-text-muted);
  transition: transform var(--transition-fast);
  flex-shrink: 0;
}}
.expandable-card__trigger[aria-expanded="true"] .expandable-card__chevron {{
  transform: rotate(180deg);
}}
.expandable-card__content {{
  max-height: 0;
  overflow: hidden;
  transition: max-height var(--transition-base) var(--ease-out-expo);
}}
.expandable-card__inner {{
  padding: 0 var(--space-4) var(--space-4);
  border-top: 1px solid var(--color-border-muted);
}}
.expandable-card__inner p {{
  color: var(--color-text-secondary);
  line-height: var(--leading-relaxed);
  margin: var(--space-3) 0;
  font-size: var(--text-sm);
}}
.expandable-card__inner ul {{
  padding-left: var(--space-5);
  margin: var(--space-2) 0;
}}
.expandable-card__inner li {{
  color: var(--color-text-secondary);
  font-size: var(--text-sm);
  margin: var(--space-1) 0;
  line-height: var(--leading-normal);
}}

/* Show more */
.show-more__toggle {{
  background: none;
  border: none;
  color: var(--color-accent);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  cursor: pointer;
  padding: var(--space-2) 0;
  transition: color var(--transition-fast);
}}
.show-more__toggle:hover {{ color: var(--color-accent-hover); }}
.show-more__toggle:focus-visible {{
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}}

/* Tables */
table {{
  width: 100%;
  border-collapse: collapse;
  margin: var(--space-3) 0;
  font-size: var(--text-sm);
}}
th {{
  text-align: left;
  padding: var(--space-2) var(--space-3);
  background: var(--color-bg-subtle);
  color: var(--color-text-secondary);
  font-weight: var(--weight-semibold);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  border-bottom: 1px solid var(--color-border);
}}
td {{
  padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--color-border-muted);
  color: var(--color-text-secondary);
}}
tr:hover td {{ background: var(--color-bg-subtle); }}
code {{
  background: var(--color-bg-muted);
  padding: 1px var(--space-2);
  border-radius: var(--radius-sm);
  font-size: 0.8em;
  font-family: var(--font-mono);
  color: var(--color-accent);
}}
.text-muted {{ color: var(--color-text-muted); font-style: italic; }}

/* Warning */
.warning {{
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  background: var(--color-warning-bg);
  border-left: 3px solid var(--color-warning-border);
  padding: var(--space-3) var(--space-4);
  margin: var(--space-4) 0;
  border-radius: var(--radius-sm);
  color: var(--color-warning-text);
  font-size: var(--text-sm);
}}
.warning svg {{ flex-shrink: 0; margin-top: 1px; }}
.report ul {{ color: var(--color-text-secondary); padding-left: var(--space-5); margin: var(--space-2) 0; }}
.report li {{ margin: var(--space-1) 0; line-height: var(--leading-relaxed); font-size: var(--text-sm); }}

/* ── Diagram tab ── */
#tab-diagram {{ position: relative; }}
.controls {{
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  flex-shrink: 0;
  z-index: 10;
}}
.controls button {{
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text-secondary);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  cursor: pointer;
  transition: all var(--transition-fast);
}}
.controls button:hover {{ background: var(--color-bg-subtle); border-color: var(--color-text-muted); }}
.controls button:focus-visible {{
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}}
.controls button.active {{
  background: var(--color-accent);
  color: var(--color-text-inverse);
  border-color: var(--color-accent);
}}

/* Search */
.search-input {{
  position: relative;
  display: flex;
  align-items: center;
}}
.search-input__icon {{
  position: absolute;
  left: var(--space-3);
  color: var(--color-text-muted);
  pointer-events: none;
}}
.search-input__icon svg {{ width: 14px; height: 14px; }}
.search-input__field {{
  padding: var(--space-2) var(--space-3) var(--space-2) var(--space-8);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--color-text-primary);
  background: var(--color-surface);
  width: 220px;
  outline: none;
  transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
}}
.search-input__field::placeholder {{ color: var(--color-text-muted); }}
.search-input__field:focus {{
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}}
.search-input__shortcut {{
  position: absolute;
  right: var(--space-2);
  padding: 1px var(--space-2);
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-bg-muted);
  color: var(--color-text-muted);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  line-height: 1.4;
}}

.zoom-group {{ margin-left: auto; display: flex; gap: var(--space-1); }}
.zoom-group button {{
  padding: var(--space-2) var(--space-3);
  font-weight: var(--weight-bold);
  min-width: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
}}
#diagram-container {{
  flex: 1;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-bg-subtle);
  position: relative;
}}
#diagram-container svg {{ max-width: none !important; max-height: none !important; }}
#diagram-error {{
  display: none;
  padding: var(--space-6);
  text-align: center;
  color: var(--color-text-muted);
}}
#diagram-loading {{
  padding: var(--space-6);
  text-align: center;
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}}

/* ── Responsive ── */
@media (max-width: 768px) {{
  body {{ flex-direction: column; }}
  .sidebar {{
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    width: 100% !important;
    min-width: 100% !important;
    height: auto;
    flex-direction: row;
    padding: var(--space-2) var(--space-4);
    z-index: 100;
    border-top: 1px solid rgba(255,255,255,0.1);
  }}
  .sidebar[data-collapsed="true"] {{ width: 100% !important; min-width: 100% !important; }}
  .sidebar__header, .sidebar__footer {{ display: none; }}
  .sidebar__nav {{
    flex-direction: row;
    gap: var(--space-1);
    flex: 1;
    justify-content: center;
  }}
  .sidebar__tab {{ flex: 1; justify-content: center; padding: var(--space-3); }}
  .sidebar__tab-label {{ font-size: var(--text-xs); }}
  .sidebar__tab-icon {{ margin: 0; }}
  main {{ padding-bottom: 60px; }}
  .report {{ padding: var(--space-4); }}
  .stat-grid {{ grid-template-columns: repeat(2, 1fr); }}
  .controls {{ flex-wrap: wrap; }}
  .search-input__field {{ width: 100%; }}
}}
@media (max-width: 480px) {{
  .stat-grid {{ grid-template-columns: 1fr; }}
}}
</style>
</head>
<body>
<a class="skip-link" href="#main-content">Skip to content</a>

<nav class="sidebar" data-collapsed="false" role="navigation" aria-label="Main navigation">
  <div class="sidebar__header">
    <h1 class="sidebar__title">{title}</h1>
    <div class="sidebar__version">HLD Viewer</div>
  </div>
  <div class="sidebar__nav" role="tablist" aria-label="Views">
    <button class="sidebar__tab" role="tab" id="tab-btn-report" aria-selected="true" aria-controls="tab-report" tabindex="0">
      <span class="sidebar__tab-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>
      <span class="sidebar__tab-label">Report</span>
    </button>
    <button class="sidebar__tab" role="tab" id="tab-btn-diagram" aria-selected="false" aria-controls="tab-diagram" tabindex="-1">
      <span class="sidebar__tab-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg></span>
      <span class="sidebar__tab-label">Architecture</span>
    </button>
  </div>
  <div class="sidebar__footer">
    <button class="sidebar__footer-btn" id="theme-toggle" aria-label="Toggle dark mode" aria-pressed="false" title="Toggle dark mode">
      <svg id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
      <svg id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:none"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
    </button>
    <button class="sidebar__footer-btn" id="sidebar-toggle" aria-label="Collapse sidebar" title="Collapse sidebar">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
    </button>
  </div>
</nav>

<main id="main-content">
  <div id="tab-report" class="tab-panel active" role="tabpanel" aria-labelledby="tab-btn-report">
    <div class="report">
      {report_html}
    </div>
  </div>

  <div id="tab-diagram" class="tab-panel" role="tabpanel" aria-labelledby="tab-btn-diagram">
    <div class="controls">
      <button id="btn-detailed" class="active">File View</button>
      <button id="btn-package">Package View</button>
      <div class="search-input" role="search">
        <span class="search-input__icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
        <input class="search-input__field" id="search" type="search" placeholder="Search nodes..." aria-label="Search diagram nodes" />
        <kbd class="search-input__shortcut">/</kbd>
      </div>
      <div class="zoom-group">
        <button id="zoom-in" title="Zoom in" aria-label="Zoom in">+</button>
        <button id="zoom-out" title="Zoom out" aria-label="Zoom out">&minus;</button>
        <button id="zoom-reset" title="Fit to screen" aria-label="Fit diagram to screen">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>
        </button>
      </div>
    </div>
    <div id="diagram-container">
      <div id="diagram-loading">Loading diagram...</div>
      <div id="diagram-error">
        <p>Diagram too large to render in this view.</p>
        <p style="margin-top:var(--space-2)">Try <strong>Package View</strong> for a simplified diagram.</p>
      </div>
    </div>
  </div>
</main>

<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
(function() {{
  'use strict';

  // ── Data ──
  const diagramDetailed = `{mermaid_detailed}`;
  const diagramPackage  = `{mermaid_package}`;

  let currentView = 'detailed';
  let panZoomInstance = null;
  let renderCounter = 0;
  let isDark = false;

  // ── Theme ──
  function initTheme() {{
    const stored = localStorage.getItem('hld-theme');
    if (stored === 'dark' || (!stored && window.matchMedia('(prefers-color-scheme: dark)').matches)) {{
      setTheme(true);
    }}
  }}

  function setTheme(dark) {{
    isDark = dark;
    document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
    document.getElementById('icon-sun').style.display = dark ? 'none' : 'block';
    document.getElementById('icon-moon').style.display = dark ? 'block' : 'none';
    document.getElementById('theme-toggle').setAttribute('aria-pressed', dark ? 'true' : 'false');
    localStorage.setItem('hld-theme', dark ? 'dark' : 'light');
    // Re-render diagram with matching theme if visible
    if (document.getElementById('tab-diagram').classList.contains('active') &&
        document.querySelector('#diagram-container svg')) {{
      mermaid.initialize({{ ...mermaidConfig, theme: dark ? 'dark' : 'default' }});
      renderDiagram(currentView === 'detailed' ? diagramDetailed : diagramPackage);
    }}
  }}

  document.getElementById('theme-toggle').addEventListener('click', () => setTheme(!isDark));

  // ── Sidebar collapse ──
  const sidebar = document.querySelector('.sidebar');
  document.getElementById('sidebar-toggle').addEventListener('click', () => {{
    const collapsed = sidebar.getAttribute('data-collapsed') === 'true';
    sidebar.setAttribute('data-collapsed', collapsed ? 'false' : 'true');
    document.getElementById('sidebar-toggle').setAttribute('aria-label',
      collapsed ? 'Collapse sidebar' : 'Expand sidebar');
  }});

  // ── Tab switching (WAI-ARIA tabs pattern) ──
  const tabs = Array.from(document.querySelectorAll('[role="tab"]'));
  const panels = Array.from(document.querySelectorAll('[role="tabpanel"]'));

  function switchTab(tab) {{
    tabs.forEach(t => {{
      t.setAttribute('aria-selected', 'false');
      t.setAttribute('tabindex', '-1');
    }});
    panels.forEach(p => p.classList.remove('active'));
    tab.setAttribute('aria-selected', 'true');
    tab.setAttribute('tabindex', '0');
    tab.focus();
    const panel = document.getElementById(tab.getAttribute('aria-controls'));
    panel.classList.add('active');
    // Lazy render diagram on first visit
    if (tab.id === 'tab-btn-diagram' && !document.querySelector('#diagram-container svg')) {{
      renderDiagram(currentView === 'detailed' ? diagramDetailed : diagramPackage);
    }}
  }}

  tabs.forEach(tab => {{
    tab.addEventListener('click', () => switchTab(tab));
    tab.addEventListener('keydown', (e) => {{
      const idx = tabs.indexOf(tab);
      if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {{
        e.preventDefault();
        switchTab(tabs[(idx + 1) % tabs.length]);
      }} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {{
        e.preventDefault();
        switchTab(tabs[(idx - 1 + tabs.length) % tabs.length]);
      }} else if (e.key === 'Home') {{
        e.preventDefault();
        switchTab(tabs[0]);
      }} else if (e.key === 'End') {{
        e.preventDefault();
        switchTab(tabs[tabs.length - 1]);
      }}
    }});
  }});

  // ── Expandable cards ──
  function initExpandableCards() {{
    document.querySelectorAll('.expandable-card__trigger').forEach(trigger => {{
      trigger.addEventListener('click', () => toggleExpand(trigger));
      trigger.addEventListener('keydown', (e) => {{
        if (e.key === 'Enter' || e.key === ' ') {{
          e.preventDefault();
          toggleExpand(trigger);
        }}
        // Arrow navigation within accordion
        if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {{
          e.preventDefault();
          const allTriggers = Array.from(
            trigger.closest('.accordion')?.querySelectorAll('.expandable-card__trigger') || []
          );
          const idx = allTriggers.indexOf(trigger);
          if (e.key === 'ArrowDown' && idx < allTriggers.length - 1) allTriggers[idx + 1].focus();
          if (e.key === 'ArrowUp' && idx > 0) allTriggers[idx - 1].focus();
        }}
        if (e.key === 'Home') {{
          e.preventDefault();
          const allTriggers = Array.from(trigger.closest('.accordion')?.querySelectorAll('.expandable-card__trigger') || []);
          if (allTriggers.length) allTriggers[0].focus();
        }}
        if (e.key === 'End') {{
          e.preventDefault();
          const allTriggers = Array.from(trigger.closest('.accordion')?.querySelectorAll('.expandable-card__trigger') || []);
          if (allTriggers.length) allTriggers[allTriggers.length - 1].focus();
        }}
      }});
    }});
  }}

  function toggleExpand(trigger) {{
    const expanded = trigger.getAttribute('aria-expanded') === 'true';
    const content = document.getElementById(trigger.getAttribute('aria-controls'));
    if (!content) return;
    trigger.setAttribute('aria-expanded', expanded ? 'false' : 'true');
    content.setAttribute('aria-hidden', expanded ? 'true' : 'false');
    if (expanded) {{
      content.style.maxHeight = content.scrollHeight + 'px';
      requestAnimationFrame(() => {{ content.style.maxHeight = '0'; }});
    }} else {{
      content.style.maxHeight = content.scrollHeight + 'px';
      const onEnd = () => {{ content.style.maxHeight = 'none'; content.removeEventListener('transitionend', onEnd); }};
      content.addEventListener('transitionend', onEnd);
    }}
  }}

  // ── Collapsible sections ──
  function initSectionToggles() {{
    document.querySelectorAll('.section__toggle').forEach(toggle => {{
      toggle.addEventListener('click', () => toggleSection(toggle));
      toggle.addEventListener('keydown', (e) => {{
        if (e.key === 'Enter' || e.key === ' ') {{
          e.preventDefault();
          toggleSection(toggle);
        }}
      }});
    }});
  }}

  function toggleSection(toggle) {{
    const expanded = toggle.getAttribute('aria-expanded') === 'true';
    const content = document.getElementById(toggle.getAttribute('aria-controls'));
    if (!content) return;
    toggle.setAttribute('aria-expanded', expanded ? 'false' : 'true');
    content.setAttribute('aria-hidden', expanded ? 'true' : 'false');
    if (expanded) {{
      content.style.maxHeight = content.scrollHeight + 'px';
      requestAnimationFrame(() => {{ content.style.maxHeight = '0'; }});
    }} else {{
      content.style.maxHeight = content.scrollHeight + 'px';
      const onEnd = () => {{ content.style.maxHeight = 'none'; content.removeEventListener('transitionend', onEnd); }};
      content.addEventListener('transitionend', onEnd);
    }}
  }}

  // ── Show more ──
  function initShowMore() {{
    document.querySelectorAll('.show-more__toggle').forEach(btn => {{
      btn.addEventListener('click', () => {{
        const container = btn.closest('.show-more');
        const items = container.querySelectorAll('.show-more__item');
        const showing = container.getAttribute('data-visible') !== '0';
        items.forEach(item => {{ item.style.display = showing ? 'none' : ''; }});
        container.setAttribute('data-visible', showing ? '0' : '1');
        btn.textContent = showing ? `Show ${{items.length}} more` : 'Show less';
      }});
    }});
  }}

  // ── Diagram rendering ──
  const mermaidConfig = {{
    startOnLoad: false,
    theme: isDark ? 'dark' : 'default',
    securityLevel: 'loose',
    maxTextSize: 500000,
    flowchart: {{ curve: 'basis', htmlLabels: true }},
  }};
  mermaid.initialize(mermaidConfig);

  async function renderDiagram(definition) {{
    if (!definition) {{ showError(); return; }}
    const container = document.getElementById('diagram-container');
    const loading = document.getElementById('diagram-loading');
    const error = document.getElementById('diagram-error');

    loading.style.display = 'block';
    error.style.display = 'none';

    if (panZoomInstance) {{
      try {{ panZoomInstance.destroy(); }} catch(e) {{}}
      panZoomInstance = null;
    }}

    try {{
      renderCounter++;
      const id = 'mermaid-diagram-' + renderCounter;
      const {{ svg }} = await mermaid.render(id, definition);
      container.innerHTML = svg;
      loading.style.display = 'none';

      const svgEl = container.querySelector('svg');
      if (svgEl) {{
        svgEl.style.width = '100%';
        svgEl.style.height = '100%';
        svgEl.setAttribute('width', '100%');
        svgEl.setAttribute('height', '100%');

        panZoomInstance = svgPanZoom(svgEl, {{
          zoomEnabled: true,
          controlIconsEnabled: false,
          fit: true,
          center: true,
          minZoom: 0.1,
          maxZoom: 20,
          zoomScaleSensitivity: 0.3,
        }});
      }}
    }} catch(e) {{
      console.error('Mermaid render error:', e);
      showError();
    }}
  }}

  function showError() {{
    document.getElementById('diagram-loading').style.display = 'none';
    document.getElementById('diagram-error').style.display = 'block';
  }}

  // ── View toggle ──
  document.getElementById('btn-detailed').addEventListener('click', () => {{
    if (currentView === 'detailed') return;
    currentView = 'detailed';
    document.getElementById('btn-detailed').classList.add('active');
    document.getElementById('btn-package').classList.remove('active');
    renderDiagram(diagramDetailed);
  }});
  document.getElementById('btn-package').addEventListener('click', () => {{
    if (currentView === 'package') return;
    currentView = 'package';
    document.getElementById('btn-package').classList.add('active');
    document.getElementById('btn-detailed').classList.remove('active');
    renderDiagram(diagramPackage);
  }});

  // ── Search ──
  let searchTimeout;
  const searchInput = document.getElementById('search');
  searchInput.addEventListener('input', (e) => {{
    clearTimeout(searchTimeout);
    searchTimeout = setTimeout(() => {{
      const query = e.target.value.toLowerCase().trim();
      const svgEl = document.querySelector('#diagram-container svg');
      if (!svgEl) return;
      const nodes = svgEl.querySelectorAll('.node');
      if (!query) {{
        nodes.forEach(n => {{ n.style.opacity = '1'; n.style.filter = ''; }});
        return;
      }}
      nodes.forEach(n => {{
        const text = (n.textContent || '').toLowerCase();
        if (text.includes(query)) {{
          n.style.opacity = '1';
          n.style.filter = 'drop-shadow(0 0 6px var(--color-accent))';
        }} else {{
          n.style.opacity = '0.15';
          n.style.filter = '';
        }}
      }});
    }}, 200);
  }});

  // ── Zoom controls ──
  document.getElementById('zoom-in').addEventListener('click', () => {{ if (panZoomInstance) panZoomInstance.zoomIn(); }});
  document.getElementById('zoom-out').addEventListener('click', () => {{ if (panZoomInstance) panZoomInstance.zoomOut(); }});
  document.getElementById('zoom-reset').addEventListener('click', () => {{
    if (panZoomInstance) {{ panZoomInstance.resetZoom(); panZoomInstance.center(); panZoomInstance.fit(); }}
  }});

  // ── Global keyboard shortcuts ──
  document.addEventListener('keydown', (e) => {{
    // '/' to focus search (when not in input)
    if (e.key === '/' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {{
      e.preventDefault();
      const searchEl = document.getElementById('search');
      // Switch to diagram tab first if needed
      const diagramTab = document.getElementById('tab-btn-diagram');
      if (diagramTab.getAttribute('aria-selected') !== 'true') {{
        switchTab(diagramTab);
      }}
      searchEl.focus();
    }}
    // Escape to clear search / close expanded
    if (e.key === 'Escape') {{
      if (document.activeElement === searchInput) {{
        searchInput.value = '';
        searchInput.dispatchEvent(new Event('input'));
        searchInput.blur();
      }}
    }}
    // Number keys 1/2 to switch tabs (when not in input)
    if ((e.key === '1' || e.key === '2') && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {{
      const idx = parseInt(e.key) - 1;
      if (idx < tabs.length) switchTab(tabs[idx]);
    }}
  }});

  // ── Init ──
  initTheme();
  initExpandableCards();
  initSectionToggles();
  initShowMore();

  // Lazy render diagram if it's the active tab on load
  if (document.getElementById('tab-diagram').classList.contains('active')) {{
    renderDiagram(diagramDetailed);
  }}
}})();
</script>
</body>
</html>"""
