<!DOCTYPE html>
<html>
<meta charset="UTF-8">

<head>
    <title>Analysis Comparison Report</title>

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap-grid.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap-reboot.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/css/bootstrap.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/15.7.1/nouislider.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />

    <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/4.6.2/js/bootstrap.bundle.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/15.7.1/nouislider.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mousetrap/1.6.5/mousetrap.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

    <style>
{% include '_base_styles.html' %}

        /* =============================================
           COMPARISON TEMPLATE SPECIFIC STYLES
           ============================================= */

        /* Sticky control row (pairs + K-slider) */
        .control-row {
            position: sticky;
            top: 36px;
            z-index: 100;
            background: white;
            padding: 0.5rem 0;
            border-bottom: 1px solid #dee2e6;
            margin-bottom: 1rem;
        }
        .pair-tabs .nav-link {
            padding: 0.4rem 0.75rem;
            border-radius: 0.25rem;
            color: #495057;
            background: #f8f9fa;
            margin-right: 0.4rem;
            margin-bottom: 0.4rem;
            border: 1px solid #dee2e6;
            font-size: 0.8rem;
        }
        .pair-tabs .nav-link:hover {
            background: #e9ecef;
            color: #007bff;
        }
        .pair-tabs .nav-link.active {
            background: #007bff;
            color: white;
            border-color: #007bff;
        }
        .pair-pane { display: none; }
        .pair-pane.active { display: block; }

        /* K-slider in floating nav */
        .k-nav-control .k-slider {
            height: 8px;
        }
        #k-slider-panel {
            padding: 0 0.25rem;
        }
        #k-slider-panel .noUi-pips {
            height: 28px;
            padding: 5px 0;
        }
        #k-slider-panel .noUi-value {
            font-size: 0.55rem;
            color: #6c757d;
            top: 12px;
        }
        #k-slider-panel .noUi-marker {
            background: #ccc;
            height: 6px;
            width: 1px;
        }
        #k-slider-panel .noUi-marker-large {
            height: 8px;
        }
        /* Special K value markers */
        #k-slider-panel .k-marker-default {
            background: #6c757d;
            width: 6px;
            height: 6px;
            margin-left: -2px;
        }
        #k-slider-panel .k-marker-elbow {
            width: 0;
            height: 0;
            background: none;
            border-left: 4px solid transparent;
            border-right: 4px solid transparent;
            border-bottom: 7px solid #ffc107;
            margin-left: -3px;
        }
        #k-slider-panel .k-marker-diminishing {
            width: 0;
            height: 0;
            background: none;
            border-left: 4px solid transparent;
            border-right: 4px solid transparent;
            border-bottom: 7px solid #17a2b8;
            margin-left: -3px;
        }

        /* Offset for sticky header when jumping to anchors */
        :target::before {
            content: "";
            display: block;
            height: 80px;
            margin-top: -80px;
        }

        @media print {
            .control-row { position: static; }
        }

        /* Lightbox overlay for enlarged images */
        .lightbox-overlay {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.85);
            z-index: 9999;
            cursor: pointer;
            justify-content: center;
            align-items: center;
        }
        .lightbox-overlay.active {
            display: flex;
        }
        .lightbox-overlay img {
            max-width: 95%;
            max-height: 95%;
            object-fit: contain;
            box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
            border-radius: 4px;
        }
        .lightbox-close {
            position: absolute;
            top: 20px;
            right: 30px;
            color: white;
            font-size: 36px;
            font-weight: bold;
            cursor: pointer;
            z-index: 10000;
        }
        .lightbox-close:hover {
            color: #ccc;
        }
        .lightbox-trigger {
            cursor: pointer;
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .lightbox-trigger:hover {
            transform: scale(1.02);
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        }

        /* Section headings within pair content */
        .section-heading {
            border-bottom: 2px solid #007bff;
            padding-bottom: 0.5rem;
            margin-top: 2rem;
            margin-bottom: 1rem;
        }
        .section-heading:first-of-type {
            margin-top: 0;
        }

        /* Note links (?) for explanations */
        .note-link {
            color: #6c757d;
            font-size: 0.75rem;
            text-decoration: none;
            vertical-align: super;
            margin-left: 0.15rem;
            padding: 0 0.2rem;
            border-radius: 50%;
            background: #f8f9fa;
            border: 1px solid #dee2e6;
        }
        .note-link:hover {
            color: #007bff;
            background: #e9ecef;
            text-decoration: none;
        }

        /* Collapsible metadata section */
        .metadata-summary {
            cursor: pointer;
            padding: 0.5rem 0.75rem;
            background: #f8f9fa;
            border-radius: 0.25rem;
            border: 1px solid #dee2e6;
        }
        .metadata-summary:hover {
            background: #e9ecef;
        }
        .metadata-full {
            padding: 0.75rem;
            background: #f8f9fa;
            border: 1px solid #dee2e6;
            border-top: none;
            border-radius: 0 0 0.25rem 0.25rem;
            font-size: 0.8rem;
        }

        /* Collapsible section toggle */
        .collapse-toggle {
            cursor: pointer;
            color: #495057;
            font-size: 0.85rem;
            user-select: none;
        }
        .collapse-toggle:hover {
            color: #007bff;
        }
        .collapse-toggle .arrow {
            display: inline-block;
            transition: transform 0.2s;
            margin-right: 0.25rem;
        }
        .collapse-toggle[aria-expanded="true"] .arrow {
            transform: rotate(90deg);
        }

        /* nouislider customisation */
        .k-slider-container {
            padding: 0.75rem;
            background: #f8f9fa;
            border-radius: 0.25rem;
            margin-bottom: 0.5rem;
            border: 1px solid #dee2e6;
        }
        .k-slider {
            height: 10px;
        }
        .noUi-connect {
            background: #007bff;
        }
        .noUi-handle {
            border-radius: 50%;
            width: 20px !important;
            height: 20px !important;
            top: -5px !important;
            right: -10px !important;
            cursor: grab;
        }
        .noUi-handle:active {
            cursor: grabbing;
        }

        /* Notes & Explanations section */
        .notes-section {
            background: #f8f9fa;
            padding: 1.5rem;
            border-radius: 0.25rem;
            margin-top: 2rem;
        }
        .notes-section h5 {
            color: #495057;
            border-bottom: 1px solid #dee2e6;
            padding-bottom: 0.5rem;
            margin-top: 1.5rem;
        }
        .notes-section h5:first-child {
            margin-top: 0;
        }

        /* CSV copy button */
        .copy-csv-btn {
            font-size: 0.7rem;
            padding: 0.2rem 0.5rem;
        }

        /* Compact header row with UMAP */
        .header-row {
            margin-bottom: 1rem;
        }
        .header-umap {
            max-height: 200px;
            cursor: pointer;
        }

        /* Info Panel (offcanvas) */
        .info-panel-overlay {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.3);
            z-index: 1040;
            opacity: 0;
            transition: opacity 0.3s ease;
        }
        .info-panel-overlay.active {
            display: block;
            opacity: 1;
        }
        .info-panel {
            position: fixed;
            top: 0;
            right: -450px;
            width: 450px;
            max-width: 90vw;
            height: 100%;
            background: white;
            box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
            z-index: 1050;
            transition: right 0.3s ease;
            display: flex;
            flex-direction: column;
        }
        .info-panel.active {
            right: 0;
        }
        .info-panel-header {
            padding: 1rem 1.25rem;
            border-bottom: 1px solid #dee2e6;
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: #f8f9fa;
            flex-shrink: 0;
        }
        .info-panel-header h5 {
            margin: 0;
            font-size: 1rem;
            font-weight: 600;
        }
        .info-panel-close {
            background: none;
            border: none;
            font-size: 1.5rem;
            line-height: 1;
            cursor: pointer;
            color: #6c757d;
            padding: 0;
        }
        .info-panel-close:hover {
            color: #343a40;
        }
        .info-panel-body {
            padding: 1.25rem;
            overflow-y: auto;
            flex-grow: 1;
        }
        .info-panel-section {
            margin-bottom: 1.5rem;
        }
        .info-panel-section:last-child {
            margin-bottom: 0;
        }
        .info-panel-section h6 {
            font-size: 0.75rem;
            text-transform: uppercase;
            letter-spacing: 0.05em;
            color: #6c757d;
            margin-bottom: 0.5rem;
            padding-bottom: 0.25rem;
            border-bottom: 1px solid #e9ecef;
        }
        .info-panel-label {
            font-weight: 600;
            color: #495057;
            margin-bottom: 0.25rem;
        }
        .info-panel-content {
            font-size: 0.875rem;
            line-height: 1.6;
            color: #212529;
        }
        .info-panel-theme {
            background: #f8f9fa;
            border-radius: 0.375rem;
            padding: 1rem;
            margin-bottom: 1rem;
        }
        .info-panel-theme:last-child {
            margin-bottom: 0;
        }
        .info-panel-theme .badge {
            font-size: 0.7rem;
        }
        .info-panel-theme-name {
            font-weight: 600;
            font-size: 0.95rem;
            margin-bottom: 0.5rem;
        }
        .info-panel-embedded {
            font-size: 0.8rem;
            color: #495057;
            line-height: 1.5;
        }
        /* Clickable elements that open info panel */
        .info-trigger {
            cursor: pointer;
        }
        .info-trigger:hover {
            text-decoration: underline;
        }
        /* Match pair display */
        .info-panel-match {
            display: flex;
            align-items: center;
            gap: 0.5rem;
            margin-bottom: 0.75rem;
            padding-bottom: 0.75rem;
            border-bottom: 1px dashed #dee2e6;
        }
        .info-panel-match:last-child {
            border-bottom: none;
            margin-bottom: 0;
            padding-bottom: 0;
        }
        .info-panel-match-arrow {
            color: #6c757d;
            font-size: 1.25rem;
        }
    </style>
</head>

<body data-spy="scroll" data-target="#floating-nav" data-offset="100">

{% set header_subtitle = 'comparison report' %}
{% include '_header.html' %}

    <!-- Floating Navigation -->
    <nav id="floating-nav" class="floating-nav card shadow-sm d-none d-xl-block">
        <div class="card-body p-2">
            <div class="nav-section">Overview</div>
            <nav class="nav flex-column">
                <a class="nav-link" href="#network">Network Plot</a>
            </nav>

            <div class="nav-section">Within Active Pair</div>
            <nav class="nav flex-column">
                <a class="nav-link pair-section-link" href="#ot-1" data-section="ot">Optimal Transport</a>
                <a class="nav-link pair-section-link" href="#matches-1" data-section="matches">Best Matches</a>
                <a class="nav-link pair-section-link" href="#similarity-1" data-section="similarity">Similarity Metrics</a>
            </nav>

            <div class="nav-section">Reference</div>
            <nav class="nav flex-column">
                <a class="nav-link" href="#themes-embeddings">Themes &amp; Embeddings</a>
                <a class="nav-link" href="#baselines">Synthetic Baselines</a>
                <a class="nav-link" href="#notes">Notes &amp; Explanations</a>
                <a class="nav-link" href="#config">Configuration</a>
            </nav>

            <!-- K-slider (shown when in OT section) -->
            <div id="k-slider-panel" class="mt-3 pt-3 border-top" style="display: none;">
                <div class="nav-section">K Parameter</div>
                {% for key, comp in comparison.by_comparisons().items() %}
                <div id="k-control-{{ loop.index }}" class="k-nav-control{% if not loop.first %} d-none{% endif %}">
                    <div id="k-slider-{{ loop.index }}" class="k-slider mb-2"></div>
                    <div class="d-flex justify-content-between align-items-center">
                        <span id="k-value-display-{{ loop.index }}" class="badge badge-primary">{{ "%.3f"|format(comp.stats.diminishing_k) }}</span>
                        <a href="#note-k-parameter" class="note-link small">?</a>
                    </div>
                    <div class="mt-2" style="font-size: 0.55rem; line-height: 1.6; color: #6c757d;">
                        <span class="text-secondary">■</span> default
                        <span class="text-warning ml-2">▲</span> elbow
                        <span class="text-info ml-2">▲</span> dim. ret.
                    </div>
                </div>
                {% endfor %}
            </div>
        </div>
    </nav>

    <script>
        // Track active pair index
        var activePairIndex = 1;

        // Switch between pair tabs
        function switchPair(index) {
            // Update active tab
            document.querySelectorAll('.pair-tabs .nav-link').forEach(function(tab, i) {
                tab.classList.toggle('active', i === index - 1);
            });
            // Update active pane
            document.querySelectorAll('.pair-pane').forEach(function(pane, i) {
                pane.classList.toggle('active', i === index - 1);
            });
            // Show/hide K-slider controls for active pair
            document.querySelectorAll('.k-nav-control').forEach(function(ctrl, i) {
                ctrl.classList.toggle('d-none', i !== index - 1);
            });
            activePairIndex = index;
            // Update pair-section-link hrefs to reflect new active pair
            document.querySelectorAll('.pair-section-link').forEach(function(link) {
                var section = link.getAttribute('data-section');
                if (section) {
                    link.setAttribute('href', '#' + section + '-' + index);
                }
            });
            // Update URL hash for bookmarking
            history.pushState(null, null, '#pair-' + index);
            // Update K-slider visibility
            updateKSliderVisibility();
        }

        // Show K-slider panel only when scrolled within OT section
        function updateKSliderVisibility() {
            var kPanel = document.getElementById('k-slider-panel');
            if (!kPanel) return;

            var otSection = document.getElementById('ot-' + activePairIndex);
            var matchesSection = document.getElementById('matches-' + activePairIndex);

            if (!otSection) {
                kPanel.style.display = 'none';
                return;
            }

            var otRect = otSection.getBoundingClientRect();
            var matchesRect = matchesSection ? matchesSection.getBoundingClientRect() : null;

            // Show if OT heading is above viewport center and matches section hasn't started
            var inOtSection = otRect.top < window.innerHeight * 0.5 &&
                              (!matchesRect || matchesRect.top > 100);

            kPanel.style.display = inOtSection ? 'block' : 'none';
        }

        // Listen for scroll to update K-slider visibility
        window.addEventListener('scroll', updateKSliderVisibility);
        document.addEventListener('DOMContentLoaded', updateKSliderVisibility);

        // Navigate to hash (handles pairs and sections)
        function navigateToHash(hash, smooth) {
            if (!hash || hash === '#') return;

            // Handle pair hashes: #pair-N
            if (hash.startsWith('#pair-')) {
                var index = parseInt(hash.replace('#pair-', ''));
                if (!isNaN(index) && index >= 1) {
                    switchPair(index);
                }
                return;
            }

            // Handle section hashes with pair suffix: #ot-1, #matches-2, etc.
            var sectionMatch = hash.match(/^#(ot|matches|similarity)-(\d+)$/);
            if (sectionMatch) {
                var pairIndex = parseInt(sectionMatch[2]);
                if (!isNaN(pairIndex) && pairIndex !== activePairIndex) {
                    // Switch to correct pair first (without updating hash)
                    document.querySelectorAll('.pair-tabs .nav-link').forEach(function(tab, i) {
                        tab.classList.toggle('active', i === pairIndex - 1);
                    });
                    document.querySelectorAll('.pair-pane').forEach(function(pane, i) {
                        pane.classList.toggle('active', i === pairIndex - 1);
                    });
                    document.querySelectorAll('.k-nav-control').forEach(function(ctrl, i) {
                        ctrl.classList.toggle('d-none', i !== pairIndex - 1);
                    });
                    activePairIndex = pairIndex;
                }
            }

            // Scroll to element
            var targetId = hash.substring(1);
            var target = document.getElementById(targetId);
            if (target) {
                setTimeout(function() {
                    target.scrollIntoView({behavior: smooth ? 'smooth' : 'auto', block: 'start'});
                    updateKSliderVisibility();
                }, 50);
            }
        }

        // Handle clicks on hash links
        document.addEventListener('click', function(e) {
            var link = e.target.closest('a[href^="#"]');
            if (!link) return;

            var href = link.getAttribute('href');
            if (!href || href === '#') return;

            // Skip links that use onclick handlers (they manage their own navigation)
            if (link.hasAttribute('onclick')) return;

            // Skip Bootstrap collapse/toggle links
            if (link.hasAttribute('data-toggle')) return;

            e.preventDefault();
            history.pushState(null, null, href);
            navigateToHash(href, true);
        });

        // Handle browser back/forward
        window.addEventListener('popstate', function() {
            navigateToHash(window.location.hash, false);
        });

        // On page load, check URL hash
        document.addEventListener('DOMContentLoaded', function() {
            var hash = window.location.hash;
            if (hash) {
                navigateToHash(hash, false);
            }
        });

        // =============================================
        // KEYBOARD SHORTCUTS (Mousetrap)
        // =============================================

        // Helper: scroll to element by ID
        function scrollTo(id) {
            var el = document.getElementById(id);
            if (el) {
                el.scrollIntoView({behavior: 'smooth', block: 'start'});
                history.pushState(null, null, '#' + id);
            }
        }

        // Helper: scroll to pair-specific section
        function scrollToPairSection(prefix) {
            var id = prefix + '-' + activePairIndex;
            scrollTo(id);
        }

        // Helper: toggle Bootstrap collapse
        function toggleCollapse(id) {
            var el = document.getElementById(id);
            if (el) {
                $(el).collapse('toggle');
            }
        }

        // Helper: adjust K slider
        function adjustK(direction) {
            var slider = sliders[activePairIndex];
            if (!slider || !slider.noUiSlider) return;

            var kValues = pairKValues[activePairIndex];
            var currentK = parseFloat(slider.noUiSlider.get());
            var currentIdx = kValues.findIndex(function(k) { return Math.abs(k - currentK) < 0.0001; });

            var newIdx = currentIdx + direction;
            if (newIdx >= 0 && newIdx < kValues.length) {
                slider.noUiSlider.set(kValues[newIdx]);
            }
        }

        // Section navigation
        Mousetrap.bind('o', function() { scrollToPairSection('ot'); return false; });
        Mousetrap.bind('m', function() { scrollToPairSection('matches'); return false; });
        Mousetrap.bind('s', function() { scrollToPairSection('similarity'); return false; });
        Mousetrap.bind('n', function() { scrollTo('network'); return false; });
        Mousetrap.bind('t', function() { scrollTo('themes-embeddings'); return false; });
        Mousetrap.bind('b', function() { scrollTo('baselines'); return false; });
        Mousetrap.bind('c', function() { scrollTo('config'); return false; });
        Mousetrap.bind('?', function() { scrollTo('notes'); return false; });

        // Toggle collapsible sections
        Mousetrap.bind('s c', function() { toggleCollapse('scree-plots-' + activePairIndex); return false; });
        Mousetrap.bind('s a', function() {
            // Scroll to sankey card (parent of iframe) within current K pane
            var kPanes = document.querySelectorAll('#pair-' + activePairIndex + ' .k-content-pane');
            kPanes.forEach(function(pane) {
                if (pane.style.display !== 'none') {
                    var iframe = pane.querySelector('iframe');
                    if (iframe) {
                        // Scroll to the card container, not the iframe itself
                        var card = iframe.closest('.card');
                        if (card) card.scrollIntoView({behavior: 'smooth', block: 'start'});
                    }
                }
            });
            return false;
        });
        Mousetrap.bind('t e', function() { toggleCollapse('themes-embeddings-content'); return false; });
        Mousetrap.bind('b a', function() { toggleCollapse('baselines-content'); return false; });

        // Pair switching (1-9)
        {% for key, comp in comparison.by_comparisons().items() %}
        Mousetrap.bind('{{ loop.index }}', function() { switchPair({{ loop.index }}); return false; });
        {% endfor %}

        // K slider navigation
        Mousetrap.bind('[', function() { adjustK(-1); return false; });
        Mousetrap.bind(']', function() { adjustK(1); return false; });
        Mousetrap.bind('k', function() { adjustK(-1); return false; });  // vim-style
        Mousetrap.bind('j', function() { adjustK(1); return false; });   // vim-style

        // Heatmaps
        Mousetrap.bind('h', function() {
            var heatmap = document.getElementById('angular-' + activePairIndex);
            if (heatmap) heatmap.scrollIntoView({behavior: 'smooth', block: 'center'});
            return false;
        });

        // Go to top
        Mousetrap.bind('g g', function() { window.scrollTo({top: 0, behavior: 'smooth'}); return false; });
        Mousetrap.bind('home', function() { window.scrollTo({top: 0, behavior: 'smooth'}); return false; });

        // Go to bottom
        Mousetrap.bind('G', function() { window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'}); return false; });
        Mousetrap.bind('end', function() { window.scrollTo({top: document.body.scrollHeight, behavior: 'smooth'}); return false; });

        // Show keyboard shortcuts help
        Mousetrap.bind('shift+?', function() {
            alert(
                'Keyboard Shortcuts\n' +
                '==================\n\n' +
                'NAVIGATION\n' +
                '  o        Optimal Transport\n' +
                '  m        Best Matches\n' +
                '  s        Similarity Metrics\n' +
                '  n        Network Plot\n' +
                '  h        Heatmap\n' +
                '  t        Themes & Embeddings\n' +
                '  b        Baselines\n' +
                '  c        Configuration\n' +
                '  ?        Notes & Help\n\n' +
                'TOGGLES\n' +
                '  s c      Toggle Scree plots\n' +
                '  s a      Scroll to Sankey\n' +
                '  t e      Toggle Themes section\n' +
                '  b a      Toggle Baselines section\n\n' +
                'PAIRS & K VALUES\n' +
                '  1-9      Switch to pair N\n' +
                '  [ / k    Previous K value\n' +
                '  ] / j    Next K value\n\n' +
                'OTHER\n' +
                '  g g      Go to top\n' +
                '  G        Go to bottom\n' +
                '  Shift+?  Show this help'
            );
            return false;
        });
    </script>

    <div class="container main-content">

        {% if text_output %}
        <!-- Text Output (collapsed) -->
        <div class="card mb-4 mt-3">
            <div class="card-header d-flex justify-content-between align-items-center">
                <a class="text-decoration-none" data-toggle="collapse" href="#textOutputPane" role="button" aria-expanded="false" aria-controls="textOutputPane">
                    <strong>▶ Plain Text Output</strong> <small class="text-muted ml-2">(click to expand, copy for reports)</small>
                </a>
                <button class="btn btn-sm btn-outline-secondary" onclick="copyTextOutput()" id="copyTextBtn">
                    📋 Copy to Clipboard
                </button>
            </div>
            <div class="collapse" id="textOutputPane">
                <div class="card-body p-0">
                    <pre id="textOutputContent" class="m-0 p-3" style="background: #f8f9fa; font-size: 0.75rem; max-height: 500px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word;">{{ text_output }}</pre>
                </div>
            </div>
        </div>
        <script>
        function copyTextOutput() {
            var text = document.getElementById('textOutputContent').innerText;
            navigator.clipboard.writeText(text).then(function() {
                var btn = document.getElementById('copyTextBtn');
                var originalText = btn.innerHTML;
                btn.innerHTML = '✓ Copied!';
                btn.classList.remove('btn-outline-secondary');
                btn.classList.add('btn-success');
                setTimeout(function() {
                    btn.innerHTML = originalText;
                    btn.classList.remove('btn-success');
                    btn.classList.add('btn-outline-secondary');
                }, 2000);
            });
        }
        </script>
        {% endif %}

        <!-- ============================================== -->
        <!-- HEADER SECTION: TITLE + UMAP + METADATA        -->
        <!-- ============================================== -->
        <h1 class="mt-4">Analysis Comparison Report</h1>

        <div class="row header-row mt-3 mb-4">
            <!-- UMAP Network Plot (3 columns) -->
            <div class="col-md-3" id="network">
                <img class="img-fluid img-thumbnail lightbox-trigger header-umap"
                     src="data:image/png;base64,{{ comparison.additional_plots.network_plot.base64 }}"
                     alt="UMAP projection of theme embeddings"
                     title="Click to enlarge">
                <p class="text-muted small mt-1 mb-0 text-center">Theme Network <a href="#note-umap" class="note-link" title="About UMAP">?</a></p>
            </div>

            <!-- Metadata Section (9 columns, collapsed by default) -->
            <div class="col-md-9">
                <div class="metadata-summary" data-toggle="collapse" data-target="#metadataFull" aria-expanded="false">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <strong>Comparing:</strong>
                            {% for result in comparison.results %}
                            <span class="badge badge-secondary ml-1">{{ result.name }} ({{ result.themes|length }} themes)</span>
                            {% endfor %}
                        </div>
                        <span class="text-muted small">▶ Click for details</span>
                    </div>
                </div>
                <div class="collapse" id="metadataFull">
                    <div class="metadata-full">
                        <div class="row">
                            <div class="col-md-6">
                                <h6 class="mb-2">Analyses</h6>
                                {% for result in comparison.results %}
                                <div class="mb-3">
                                    <strong>{{ result.name }}</strong>
                                    <span class="text-muted small">({{ result.themes|length }} themes)</span>
                                    <ul class="small mb-0 pl-3">
                                        {% for t in result.themes %}
                                        <li>{{ t.name }}</li>
                                        {% endfor %}
                                    </ul>
                                </div>
                                {% endfor %}
                            </div>
                            <div class="col-md-6">
                                <h6 class="mb-2">Configuration</h6>
                                <ul class="list-unstyled small mb-0">
                                    <li><strong>Threshold:</strong> {{ comparison.config.threshold }}</li>
                                    <li><strong>Distance:</strong> {{ comparison.config.distance | default('angular') }}</li>
                                    <li><strong>Embedding model:</strong> {{ comparison.config.embedding_model | default('text-embedding-3-large') }}</li>
                                    <li><strong>K (reg_m):</strong> {{ comparison.config.reg_m | default(0.2) }}</li>
                                    {% if comparison.config.compute_paraphrase_bound is not false %}
                                    <li><strong>Paraphrase baseline:</strong> enabled ({{ comparison.config.n_paraphrases | default(3) }} per theme)</li>
                                    {% endif %}
                                </ul>
                                <a href="#config" class="small text-muted d-block mt-2">View full configuration →</a>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>

        <!-- ============================================== -->
        <!-- QUICK REFERENCE: THEME LABELS                  -->
        <!-- ============================================== -->
        <div class="mt-4 mb-4">
            <h5>
                <a class="collapse-toggle text-dark" data-toggle="collapse" href="#theme-reference" role="button" aria-expanded="true">
                    <span class="arrow">▼</span> Theme Quick Reference
                </a>
                <small class="text-muted ml-2">Short labels generated by LLM and used throughout this report</small>
            </h5>
            <div class="collapse show" id="theme-reference">
                <div class="row">
                    {% for analysis_name, themes in comparison.embedded_strings.items() %}
                    {% set set_letter = themes[0].set_letter if themes else 'X' %}
                    <div class="col-md-4 mb-3">
                        <div class="card">
                            <div class="card-header py-2 bg-light">
                                <span class="badge badge-primary mr-1">{{ set_letter }}</span>
                                <strong>{{ analysis_name }}</strong>
                                <span class="badge badge-secondary">{{ themes | length }} themes</span>
                            </div>
                            <div class="card-body p-2">
                                {% for item in themes %}
                                <div class="mb-2 pb-2 {% if not loop.last %}border-bottom{% endif %}">
                                    <a href="#theme-{{ analysis_name | replace(' ', '-') }}-{{ item.theme_index }}"
                                       class="theme-label-link text-primary"
                                       style="font-size: 0.75rem; font-weight: 600;"
                                       data-toggle="tooltip"
                                       data-placement="right"
                                       data-html="true"
                                       title="<strong>{{ item.theme_name }}</strong>">
                                        {{ item.set_letter }}{{ item.theme_index }}: {{ item.short_label | default('Theme ' ~ item.theme_index) }}
                                    </a>
                                    <div class="text-muted" style="font-size: 0.7rem; line-height: 1.3; max-height: 3.9em; overflow: hidden;">
                                        {{ item.embedded_string }}
                                    </div>
                                </div>
                                {% endfor %}
                            </div>
                        </div>
                    </div>
                    {% endfor %}
                </div>
            </div>
        </div>

        <hr class="mt-2 mb-4">

        <!-- ============================================== -->
        <!-- PAIRWISE COMPARISONS                           -->
        <!-- ============================================== -->
        <h2 id="pairwise">Pairwise Comparisons</h2>
            <p class="text-muted">Select a pair to view detailed comparison metrics.</p>

            <!-- Sticky control row: pair tabs -->
            <div class="control-row">
                <nav class="nav pair-tabs flex-wrap">
                    {% for key, comp in comparison.by_comparisons().items() %}
                    <a class="nav-link{% if loop.first %} active{% endif %}" href="#" onclick="switchPair({{ loop.index }}); return false;">
                        {{ comp.a.name }} vs {{ comp.b.name }}
                    </a>
                    {% endfor %}
                </nav>
            </div>

            <!-- Tab panes for each pair -->
            {% for key, comp in comparison.by_comparisons().items() %}
            {% set pair_idx = loop.index %}
            <div id="pair-{{ pair_idx }}" class="pair-pane{% if loop.first %} active{% endif %}">
                <h3 class="mb-3">{{ comp.a.name }} vs {{ comp.b.name }}</h3>

                <!-- ============================================== -->
                <!-- SECTION: OPTIMAL TRANSPORT                     -->
                <!-- ============================================== -->
                <h4 id="ot-{{ pair_idx }}" class="section-heading">Unbalanced Optimal Transport <a href="#note-optimal-transport" class="note-link" title="What is optimal transport?">?</a></h4>
                <p class="text-muted small mb-3">
                    Many-to-many alignment, but allows themes to remain unmatched when no good match exists.
                </p>

                <!-- Scree plots (collapsible) -->
                <div class="mb-3">
                    <a class="collapse-toggle small" data-toggle="collapse" href="#scree-plots-{{ pair_idx }}" role="button" aria-expanded="false" aria-controls="scree-plots-{{ pair_idx }}">
                        <span class="arrow">▶</span> Scree Plots (how metrics vary with K)
                    </a>
                    <div class="collapse" id="scree-plots-{{ pair_idx }}">
                        <div class="row mt-2 mb-3">
                            <div class="col-md-4">
                                <img class="img-fluid img-thumbnail lightbox-trigger" src="data:image/png;base64,{{ comp.stats.shared_mass_scree.base64 }}" alt="Shared mass" title="Shared Mass vs K">
                            </div>
                            {% if comp.stats.alignment_scree %}
                            <div class="col-md-4">
                                <img class="img-fluid img-thumbnail lightbox-trigger" src="data:image/png;base64,{{ comp.stats.alignment_scree.base64 }}" alt="Alignment" title="Alignment vs K">
                            </div>
                            {% endif %}
                            {% if comp.stats.splits_joins_scree %}
                            <div class="col-md-4">
                                <img class="img-fluid img-thumbnail lightbox-trigger" src="data:image/png;base64,{{ comp.stats.splits_joins_scree.base64 }}" alt="Splits/Joins" title="Splits/Joins vs K">
                            </div>
                            {% endif %}
                        </div>
                        <p class="text-muted small mb-0">Green = paraphrase ceiling, Red = word-salad floor.</p>
                    </div>
                </div>

                <!-- Per-K content panes (shown/hidden by slider, K >= 0.1 only) -->
                {% for k_val in comp.stats.k_values if k_val >= 0.1 %}
                {% set ot_k = comp.stats.ot_by_k[k_val] %}
                <div id="k-content-{{ pair_idx }}-{{ "%.3f"|format(k_val)|replace('.', '-') }}" class="k-content-pane" style="{% if k_val != comp.stats.diminishing_k %}display: none;{% endif %}">

                    <!-- Stats summary (compact horizontal) -->
                    <div class="card mb-3 bg-light">
                        <div class="card-header py-1 bg-light border-bottom">
                            <strong>K = {{ k_val }}</strong>
                            <a href="#note-k-parameter" class="note-link">?</a>
                            {% if k_val == comp.stats.diminishing_k %}<span class="badge badge-info ml-2">diminishing returns</span>{% endif %}
                            {% if k_val == comp.stats.default_k %}<span class="badge badge-secondary ml-1">default</span>{% endif %}
                        </div>
                        <div class="card-body py-2">
                            <div class="d-flex flex-wrap align-items-center justify-content-between small">
                                <div>
                                    <strong>Shared Mass <a href="#note-shared-mass" class="note-link">?</a>:</strong> {{ "%.1f"|format(ot_k.ot.shared_mass * 100) }}%
                                    {% if ot_k.ot.shared_mass_pct_of_ceiling is defined %}
                                    <span class="text-muted">({{ "%.1f"|format(ot_k.ot.shared_mass_pct_of_ceiling * 100) }}% of ceiling)</span>
                                    {% endif %}
                                </div>
                                <div>
                                    <strong>Alignment <a href="#note-alignment" class="note-link">?</a>:</strong> {% if ot_k.ot.alignment_observed is defined %}{{ "%.1f"|format(ot_k.ot.alignment_observed) }}{% else %}{{ "%.1f"|format(1 - ot_k.ot.avg_cost) }}{% endif %}
                                </div>
                                <div>
                                    <strong>Effect <a href="#note-effect-size" class="note-link">?</a>:</strong> {{ "%.1f"|format(ot_k.ot.shared_mass_effect) }} MADs
                                </div>
                                <div>
                                    {% if ot_k.ot.filter_stats is defined and ot_k.ot.filter_stats.filtering_enabled %}
                                    <strong>Splits:</strong> {{ "%.1f"|format(ot_k.ot.filtered_split_join_stats.splits_from_a.mean) }} avg
                                    <strong class="ml-2">Joins:</strong> {{ "%.1f"|format(ot_k.ot.filtered_split_join_stats.joins_to_b.mean) }} avg
                                    <span class="text-muted ml-1">(filtered)</span>
                                    {% else %}
                                    <strong>Splits:</strong> {{ "%.1f"|format(ot_k.split_join_stats.splits_from_a.mean) }} avg
                                    <strong class="ml-2">Joins:</strong> {{ "%.1f"|format(ot_k.split_join_stats.joins_to_b.mean) }} avg
                                    {% endif %}
                                </div>
                            </div>
                            <!-- Filtering info -->
                            {% if ot_k.ot.filter_stats is defined and ot_k.ot.filter_stats.filtering_enabled %}
                            <div class="small text-muted mt-1">
                                <em>Transport filtered at {{ "%.0f"|format(ot_k.ot.filter_stats.threshold * 100) }}% threshold: {{ "%.1f"|format(ot_k.ot.filter_stats.mass_retained_pct) }}% mass retained, {{ ot_k.ot.filter_stats.edges_filtered }}/{{ ot_k.ot.filter_stats.edges_original }} edges</em>
                            </div>
                            {% endif %}
                            <!-- Cost matrix structure (theme preferences) -->
                            {% if ot_k.ot.cost_structure is defined %}
                            <div class="small text-muted mt-1">
                                <em>Theme preferences: {{ "%.0f"|format(ot_k.ot.cost_structure.relative_preference * 100) }}% relative preference <a href="#note-theme-preferences" class="note-link">?</a> — {{ ot_k.ot.cost_structure.interpretation }}</em>
                            </div>
                            {% endif %}
                            <!-- Expandable baseline details -->
                            {% if ot_k.ot.paraphrase_upper_bound is defined or ot_k.ot.null_shared_mass_mean is defined or ot_k.ot.cost_structure is defined %}
                            <div class="mt-2 pt-2 border-top">
                                <a class="collapse-toggle small text-muted" data-toggle="collapse" href="#baseline-details-{{ pair_idx }}-{{ k_val|replace('.', '-') }}" role="button" aria-expanded="false">
                                    <span class="arrow">▶</span> Show baseline values and theme structure
                                </a>
                                <div class="collapse" id="baseline-details-{{ pair_idx }}-{{ k_val|replace('.', '-') }}">
                                    <div class="row mt-2 small">
                                        <div class="col-md-6">
                                            <strong>Shared Mass Baselines:</strong>
                                            <ul class="list-unstyled mb-0 ml-3">
                                                {% if ot_k.ot.paraphrase_upper_bound is defined %}
                                                <li><span class="text-success">▲</span> Paraphrase ceiling <a href="#note-paraphrase-ceiling" class="note-link">?</a>: {{ "%.1f"|format(ot_k.ot.paraphrase_upper_bound * 100) }}%</li>
                                                {% endif %}
                                                {% if ot_k.ot.null_shared_mass_mean is defined %}
                                                <li><span class="text-danger">▼</span> Word-salad floor <a href="#note-wordsalad-floor" class="note-link">?</a>: {{ "%.1f"|format(ot_k.ot.null_shared_mass_mean * 100) }}%</li>
                                                {% endif %}
                                            </ul>
                                        </div>
                                        <div class="col-md-6">
                                            {% if ot_k.ot.cost_structure is defined %}
                                            <strong>Theme Preference Structure <a href="#note-theme-preferences" class="note-link">?</a>:</strong>
                                            <ul class="list-unstyled mb-0 ml-3">
                                                <li>Relative preference: {{ "%.0f"|format(ot_k.ot.cost_structure.relative_preference * 100) }}%</li>
                                                <li>Preference strength: {{ "%.3f"|format(ot_k.ot.cost_structure.avg_preference_strength) }}</li>
                                                <li>Spikiness: {{ "%.2f"|format(ot_k.ot.cost_structure.spikiness) }}</li>
                                                <li class="text-muted small">{{ ot_k.ot.cost_structure.interpretation }}</li>
                                            </ul>
                                            {% else %}
                                            <strong>Alignment:</strong>
                                            <ul class="list-unstyled mb-0 ml-3">
                                                {% if ot_k.ot.alignment_paraphrase_ceiling is defined %}
                                                <li><span class="text-success">▲</span> Paraphrase ceiling <a href="#note-paraphrase-ceiling" class="note-link">?</a>: {{ "%.1f"|format(ot_k.ot.alignment_paraphrase_ceiling) }}</li>
                                                {% endif %}
                                                {% if ot_k.ot.alignment_null_floor is defined %}
                                                <li><span class="text-danger">▼</span> Word-salad floor <a href="#note-wordsalad-floor" class="note-link">?</a>: {{ "%.1f"|format(ot_k.ot.alignment_null_floor) }}</li>
                                                {% endif %}
                                            </ul>
                                            {% endif %}
                                        </div>
                                    </div>
                                </div>
                            </div>
                            {% endif %}
                        </div>
                    </div>

                    <!-- Sankey diagram with dual color mode toggle -->
                    <div class="card mb-3">
                        <div class="card-header py-2 d-flex justify-content-between align-items-center">
                            <strong>Transport Flow <a href="#note-sankey-colours" class="note-link">?</a></strong>
                            <div class="btn-group btn-group-sm sankey-color-toggle" role="group" aria-label="Sankey colour mode">
                                <button type="button" class="btn btn-outline-primary active" data-mode="sim">Similarity</button>
                                <button type="button" class="btn btn-outline-primary" data-mode="k-rel">K-relative</button>
                            </div>
                        </div>
                        <div class="card-body p-2">
                            <div class="sankey-frame" data-mode="sim">
                                <iframe srcdoc="{{ ot_k.transport_sankey.html | e }}" style="width: 100%; height: 660px; border: none;" frameborder="0"></iframe>
                            </div>
                            <div class="sankey-frame" data-mode="k-rel" style="display: none;">
                                <iframe srcdoc="{{ ot_k.transport_sankey_k_rel.html | e }}" style="width: 100%; height: 660px; border: none;" frameborder="0"></iframe>
                            </div>
                        </div>
                    </div>

                    <div class="row">
                        <!-- Transport plan heatmap -->
                        <div class="col-md-6">
                            <div class="card mb-3">
                                <div class="card-header py-2">
                                    <strong>Transport Matrix <a href="#note-transport-heatmap" class="note-link">?</a></strong>
                                </div>
                                <div class="card-body p-2">
                                    <img class="img-fluid img-thumbnail lightbox-trigger" src="data:image/png;base64,{{ ot_k.transport_heatmap.base64 }}" alt="Transport heatmap">
                                </div>
                            </div>
                        </div>

                        <!-- Many-to-many best matches -->
                        <div class="col-md-6">
                            <div class="card mb-3">
                                <div class="card-header py-2">
                                    <strong>Many-to-Many Matches</strong>
                                </div>
                                <div class="card-body p-2">
                                    <div class="row">
                                        <div class="col-6">
                                            <p class="small text-muted mb-1">{{ comp.a.name }} →</p>
                                            <table class="table table-sm table-striped small mb-0">
                                                <tbody>
                                                    {% for match in ot_k.best_matches_a_to_b[:5] %}
                                                    {% set theme_a = comp.embedded_a[match.theme_a_index] %}
                                                    {% set theme_b = comp.embedded_b[match.theme_b_index] %}
                                                    <tr>
                                                        <td><small><a href="#theme-{{ comp.a.name | replace(' ', '-') }}-{{ theme_a.theme_index }}" class="theme-label-link" data-toggle="tooltip" data-html="true" title="{{ theme_a.theme_name }}">{{ theme_a.set_letter }}{{ theme_a.theme_index }}: {{ theme_a.short_label | default(theme_a.theme_name[:12]) }}</a></small></td>
                                                        <td class="text-muted">→</td>
                                                        <td><small><a href="#theme-{{ comp.b.name | replace(' ', '-') }}-{{ theme_b.theme_index }}" class="theme-label-link" data-toggle="tooltip" data-html="true" title="{{ theme_b.theme_name }}">{{ theme_b.set_letter }}{{ theme_b.theme_index }}: {{ theme_b.short_label | default(theme_b.theme_name[:12]) }}</a></small></td>
                                                        <td class="text-right">{{ "%.0f"|format(match.mass_total * 100) }}%</td>
                                                    </tr>
                                                    {% endfor %}
                                                </tbody>
                                            </table>
                                        </div>
                                        <div class="col-6">
                                            <p class="small text-muted mb-1">{{ comp.b.name }} →</p>
                                            <table class="table table-sm table-striped small mb-0">
                                                <tbody>
                                                    {% for match in ot_k.best_matches_b_to_a[:5] %}
                                                    {% set theme_b = comp.embedded_b[match.theme_b_index] %}
                                                    {% set theme_a = comp.embedded_a[match.theme_a_index] %}
                                                    <tr>
                                                        <td><small><a href="#theme-{{ comp.b.name | replace(' ', '-') }}-{{ theme_b.theme_index }}" class="theme-label-link" data-toggle="tooltip" data-html="true" title="{{ theme_b.theme_name }}">{{ theme_b.set_letter }}{{ theme_b.theme_index }}: {{ theme_b.short_label | default(theme_b.theme_name[:12]) }}</a></small></td>
                                                        <td class="text-muted">→</td>
                                                        <td><small><a href="#theme-{{ comp.a.name | replace(' ', '-') }}-{{ theme_a.theme_index }}" class="theme-label-link" data-toggle="tooltip" data-html="true" title="{{ theme_a.theme_name }}">{{ theme_a.set_letter }}{{ theme_a.theme_index }}: {{ theme_a.short_label | default(theme_a.theme_name[:12]) }}</a></small></td>
                                                        <td class="text-right">{{ "%.0f"|format(match.mass_total * 100) }}%</td>
                                                    </tr>
                                                    {% endfor %}
                                                </tbody>
                                            </table>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>

                </div><!-- end k-content-pane -->
                {% endfor %}

                <!-- ============================================== -->
                <!-- SECTION: BEST MATCHES                          -->
                <!-- ============================================== -->
                <h4 id="matches-{{ pair_idx }}" class="section-heading">Best Matches</h4>

                <!-- Coverage & Hit Rates -->
                <h5 id="summary-{{ pair_idx }}">Coverage &amp; Hit Rates <a href="#note-coverage-hitrates" class="note-link" title="About coverage metrics">?</a></h5>
                <p class="text-muted small">Thematic analysis doesn't have ground truth, so traditional precision/recall don't apply. Instead, we measure coverage (did themes find matches?) and fidelity (how close are the best matches?). Based on {{ comp.stats.similarity_metric }} similarity.
                {% if comp.stats.calibration_path %}
                <span class="badge badge-success">Using calibrated values</span>
                {% endif %}
                </p>
                <div class="row mb-4">
                    <div class="col-md-6">
                        <div class="card h-100">
                            <div class="card-header bg-info text-white">Coverage (Hit Rates)</div>
                            <div class="card-body">
                                <p class="small text-muted">Proportion of themes with at least one match above threshold ({{ comparison.config.threshold }})</p>
                                <ul class="list-unstyled">
                                    <li><strong>Hit Rate A:</strong> {{ "%.1f"|format(comp.stats.hit_rate_a * 100) }}%</li>
                                    <li><strong>Hit Rate B:</strong> {{ "%.1f"|format(comp.stats.hit_rate_b * 100) }}%</li>
                                    <li class="mt-2"><strong>Pair Match Rate:</strong> {{ "%.3f"|format(comp.stats.jaccard) }} <small class="text-muted">(pairs above threshold / total pairs)</small></li>
                                </ul>
                                <p class="small mt-2 text-muted"><em>High hit rates indicate both analyses found similar conceptual territory.</em></p>
                            </div>
                        </div>
                    </div>

                    <div class="col-md-6">
                        <div class="card h-100">
                            <div class="card-header bg-success text-white">Fidelity (Match Quality)</div>
                            <div class="card-body">
                                <p class="small text-muted">How close are the best matches? (Mean of each theme's best match similarity)</p>
                                <ul class="list-unstyled">
                                    <li><strong>A→B:</strong> {{ "%.3f"|format(comp.stats.mean_max_sim_a_to_b) }}</li>
                                    <li><strong>B→A:</strong> {{ "%.3f"|format(comp.stats.mean_max_sim_b_to_a) }}</li>
                                    <li class="mt-2"><strong>Fidelity:</strong> {{ "%.3f"|format(comp.stats.fidelity) }}</li>
                                </ul>
                                <p class="small mt-2 text-muted"><em>Fidelity is the harmonic mean of directional scores. Higher = tighter semantic alignment.</em></p>
                            </div>
                        </div>
                    </div>
                </div>

                <hr>

                <!-- ============================================== -->
                <!-- SECTION 3: HUNGARIAN MATCHING                  -->
                <!-- ============================================== -->
                <h5 id="hungarian-{{ pair_idx }}">Hungarian Matching (1:1) <a href="#note-hungarian" class="note-link" title="About Hungarian algorithm">?</a></h5>
                <p class="text-muted">The Hungarian algorithm finds the optimal one-to-one pairing that maximizes total similarity. Each theme maps to at most one theme in the other set -- no reuse allowed.
                {% if comp.stats.calibration_path %}
                <span class="badge badge-success">Using calibrated values</span>
                {% endif %}
                </p>

                <div class="alert alert-secondary small mb-3">
                    <strong>Intuition:</strong> "If I had to explain set B's themes to someone who only knew set A, which single theme in A would each B theme correspond to, with no reuse?"
                    <br><br>
                    <strong>What this enables:</strong> Hungarian matching removes ambiguity by assigning each theme to at most one partner. Coverage metrics show what proportion of each set found a good match.
                    <br><br>
                    <strong>Limitation:</strong> This penalises legitimate theme refinement (splitting one theme into two is treated as unmatched). Use OT if you want to allow many-to-many alignment.
                </div>

                <div class="row mb-4">
                    <div class="col-md-6">
                        <div class="card border-primary">
                            <div class="card-header bg-primary text-white">
                                <strong>Mean Matched Similarity</strong> (primary metric)
                            </div>
                            <div class="card-body">
                                <div class="text-center mb-3">
                                    <h2 class="text-primary mb-0">{{ "%.3f"|format(comp.stats.hungarian.soft_metrics.soft_precision) }}</h2>
                                    <p class="small text-muted">Average similarity of optimal pairs</p>
                                </div>
                                <p class="small"><strong>Interpretation:</strong> "How good are the best one-to-one correspondences?" Higher = tighter semantic alignment between the two theme sets.</p>

                                {% if comp.stats.hungarian.distribution.n_pairs > 0 %}
                                <hr>
                                <p class="small mb-1"><strong>Distribution of {{ comp.stats.hungarian.distribution.n_pairs }} optimal pairs:</strong></p>
                                <ul class="list-unstyled small">
                                    <li>Median: {{ "%.3f"|format(comp.stats.hungarian.distribution.median) }} &nbsp; (Q1: {{ "%.3f"|format(comp.stats.hungarian.distribution.q1) }}, Q3: {{ "%.3f"|format(comp.stats.hungarian.distribution.q3) }})</li>
                                    <li>Range: {{ "%.3f"|format(comp.stats.hungarian.distribution.min) }} -- {{ "%.3f"|format(comp.stats.hungarian.distribution.max) }}</li>
                                </ul>
                                {% endif %}
                            </div>
                        </div>
                    </div>

                    <div class="col-md-6">
                        <div class="card">
                            <div class="card-header bg-secondary text-white">
                                <strong>Coverage &amp; Set Overlap</strong>
                            </div>
                            <div class="card-body">
                                <p class="small text-muted">Based on {{ comp.stats.hungarian.distribution.n_pairs }} matched pairs above threshold ({{ comparison.config.threshold }})</p>
                                {% set cov_a = comp.stats.hungarian.thresholded_metrics.recall %}
                                {% set cov_b = comp.stats.hungarian.thresholded_metrics.precision %}
                                {% set mean_cov = (cov_a + cov_b) / 2 %}
                                <div class="row text-center">
                                    <div class="col-4">
                                        <h4 class="mb-0">{{ "%.0f"|format(cov_a * 100) }}%</h4>
                                        <p class="small text-muted mb-0">Coverage A</p>
                                        <p class="small text-muted">(A themes matched)</p>
                                    </div>
                                    <div class="col-4">
                                        <h4 class="mb-0">{{ "%.0f"|format(cov_b * 100) }}%</h4>
                                        <p class="small text-muted mb-0">Coverage B</p>
                                        <p class="small text-muted">(B themes matched)</p>
                                    </div>
                                    <div class="col-4">
                                        <h4 class="mb-0">{{ "%.3f"|format(comp.stats.hungarian.thresholded_metrics.true_jaccard) }}</h4>
                                        <p class="small text-muted mb-0">Jaccard Index</p>
                                        <p class="small text-muted">(set overlap)</p>
                                    </div>
                                </div>
                                <hr>
                                <p class="small mb-0"><strong>Jaccard Index</strong> = matched / (|A| + |B| - matched). Measures overlap between theme sets after 1:1 assignment. Higher = more themes found good partners.</p>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="card mb-4">
                    <div class="card-header">
                        <strong>Optimal Matched Pairs ({{ comp.stats.hungarian.all_pairs|length }})</strong>
                    </div>
                    <div class="card-body">
                        {% if comp.stats.hungarian.all_pairs|length > 0 %}
                        <p class="small text-muted">Hungarian algorithm finds the optimal one-to-one assignment.
                        {% if comp.stats.calibration_path %}
                        Similarity values are calibrated.
                        {% endif %}
                        </p>
                        <table class="table table-sm table-striped">
                            <thead>
                                <tr>
                                    <th width="35%">Theme in {{ comp.a.name }}</th>
                                    <th width="35%">Theme in {{ comp.b.name }}</th>
                                    {% if comp.stats.calibration_path %}
                                    <th width="15%">Calibrated</th>
                                    <th width="15%">Raw Angular</th>
                                    {% else %}
                                    <th width="30%">Angular Similarity</th>
                                    {% endif %}
                                </tr>
                            </thead>
                            <tbody>
                                {% for pair_data in comp.stats.hungarian.all_pairs %}
                                {% set i = pair_data[0] %}
                                {% set j = pair_data[1] %}
                                {% set similarity = pair_data[2] %}
                                {% set similarity_raw = pair_data[3] if pair_data|length > 3 else similarity %}
                                {% set theme_a = comp.embedded_a[i] %}
                                {% set theme_b = comp.embedded_b[j] %}
                                <tr>
                                    <td class="small">
                                        <a href="#theme-{{ comp.a.name | replace(' ', '-') }}-{{ theme_a.theme_index }}"
                                           class="theme-label-link text-primary"
                                           data-toggle="tooltip" data-html="true"
                                           title="<strong>{{ theme_a.theme_name }}</strong><br><small>{{ theme_a.embedded_string[:150] }}{% if theme_a.embedded_string|length > 150 %}...{% endif %}</small>">
                                            {{ theme_a.set_letter }}{{ theme_a.theme_index }}: {{ theme_a.short_label | default(theme_a.theme_name[:18]) }}
                                        </a>
                                    </td>
                                    <td class="small">
                                        <a href="#theme-{{ comp.b.name | replace(' ', '-') }}-{{ theme_b.theme_index }}"
                                           class="theme-label-link text-primary"
                                           data-toggle="tooltip" data-html="true"
                                           title="<strong>{{ theme_b.theme_name }}</strong><br><small>{{ theme_b.embedded_string[:150] }}{% if theme_b.embedded_string|length > 150 %}...{% endif %}</small>">
                                            {{ theme_b.set_letter }}{{ theme_b.theme_index }}: {{ theme_b.short_label | default(theme_b.theme_name[:18]) }}
                                        </a>
                                    </td>
                                    {% if comp.stats.calibration_path %}
                                    <td class="small text-center">{{ "%.3f"|format(similarity) }}</td>
                                    <td class="small text-center text-muted">{{ "%.3f"|format(similarity_raw) }}</td>
                                    {% else %}
                                    <td class="small text-center">{{ "%.3f"|format(similarity) }}</td>
                                    {% endif %}
                                </tr>
                                {% endfor %}
                            </tbody>
                        </table>
                        {% else %}
                        <p class="text-muted">No optimal pairs found.</p>
                        {% endif %}
                    </div>
                </div>

                <!-- ============================================== -->
                <!-- SECTION: SIMILARITY METRICS                    -->
                <!-- ============================================== -->
                <h4 id="similarity-{{ pair_idx }}" class="section-heading">Similarity Metrics</h4>
                <p class="text-muted small">Various distance and similarity metrics between theme embeddings.</p>

                {% if comp.stats.calibration_path %}
                <!-- Calibrated Similarity (PRIMARY - used for all statistics) -->
                <h5>
                    <span class="badge badge-success">Primary</span>
                    Calibrated Similarity
                </h5>
                <p class="text-muted small">
                    <strong>All statistics (fidelity, coverage, thresholding, Hungarian matching) use these calibrated values.</strong>
                    Similarities transformed using a calibration model trained on paraphrases.
                </p>
                <div class="row mb-4">
                    <div class="col-md-6">
                        <div class="card">
                            <div class="card-header bg-success text-white d-flex justify-content-between align-items-center">
                                Continuous Values (Calibrated)
                            </div>
                            <div class="card-body">
                                {% if comp.plots.heatmaps_rescaled %}
                                <img class="img-fluid img-thumbnail lightbox-trigger" src="data:image/png;base64,{{ comp.plots.heatmaps_rescaled.base64 }}" alt="Calibrated similarity heatmap">
                                {% endif %}
                            </div>
                        </div>
                    </div>
                    <div class="col-md-6">
                        <div class="card">
                            <div class="card-header bg-success text-white">Binary Match (threshold={{ comparison.config.threshold }})</div>
                            <div class="card-body">
                                {% if comp.plots.heatmaps_rescaled_thresholded %}
                                <img class="img-fluid img-thumbnail lightbox-trigger" src="data:image/png;base64,{{ comp.plots.heatmaps_rescaled_thresholded.base64 }}" alt="Calibrated thresholded heatmap">
                                {% endif %}
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Calibrated similarity matrix as table -->
                <div class="card mb-4">
                    <div class="card-header bg-success text-white">
                        <a class="text-white collapse-toggle" data-toggle="collapse" href="#rescaled-matrix-table-{{ pair_idx }}">
                            <span class="arrow">▶</span> Calibrated Similarity Matrix (Table)
                        </a>
                    </div>
                    <div class="collapse" id="rescaled-matrix-table-{{ pair_idx }}">
                        <div class="card-body table-responsive">
                            <table class="table table-sm table-bordered" style="font-size: 0.75rem;">
                                <thead>
                                    <tr>
                                        <th></th>
                                        {% for j in range(comp.stats.selected_similarity_matrix[0]|length) %}
                                        <th class="text-center" style="writing-mode: vertical-rl; transform: rotate(180deg); max-width: 30px;">{{ j+1 }}</th>
                                        {% endfor %}
                                    </tr>
                                </thead>
                                <tbody>
                                    {% for i, row in enumerate(comp.stats.selected_similarity_matrix) %}
                                    <tr>
                                        <th>{{ i+1 }}</th>
                                        {% for val in row %}
                                        <td class="text-center {% if val >= comparison.config.threshold %}table-success{% endif %}">{{ "%.2f"|format(val) }}</td>
                                        {% endfor %}
                                    </tr>
                                    {% endfor %}
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
                {% endif %}

                <!-- Raw Angular Similarity Heatmaps -->
                <h5>
                    {% if comp.stats.calibration_path %}
                    <span class="badge badge-secondary">Raw</span>
                    {% endif %}
                    Angular Similarity <a href="#note-angular-distance" class="note-link" title="About angular distance">?</a>
                </h5>
                <p class="text-muted small">Angular distance uses the angle between embedding vectors (arccos of cosine), normalised to [0,1]. Unlike cosine, it satisfies the triangle inequality, making it a proper metric.
                {% if comp.stats.calibration_path %}<strong class="text-warning">These are RAW values before calibration.</strong>{% endif %}
                </p>
                <div class="row mb-4">
                    <div class="col-md-6">
                        <div class="card">
                            <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
                                Continuous Values (Raw)
                                <button class="btn btn-sm btn-light copy-csv-btn" onclick="copyMatrixCSV('angular-{{ pair_idx }}')">Copy CSV</button>
                            </div>
                            <div class="card-body">
                                <img class="img-fluid img-thumbnail lightbox-trigger" id="angular-{{ pair_idx }}" src="data:image/png;base64,{{ comp.plots.heatmaps_angle.base64 }}" alt="Angular distance heatmap">
                            </div>
                        </div>
                    </div>
                    <div class="col-md-6">
                        <!-- Raw angular matrix as table -->
                        <div class="card h-100">
                            <div class="card-header bg-secondary text-white">
                                <a class="text-white collapse-toggle" data-toggle="collapse" href="#angular-matrix-table-{{ pair_idx }}">
                                    <span class="arrow">▶</span> Raw Angular Matrix (Table)
                                </a>
                            </div>
                            <div class="collapse" id="angular-matrix-table-{{ pair_idx }}">
                                <div class="card-body table-responsive">
                                    <table class="table table-sm table-bordered" style="font-size: 0.75rem;">
                                        <thead>
                                            <tr>
                                                <th></th>
                                                {% for j in range(comp.stats.angle_similarity_matrix[0]|length) %}
                                                <th class="text-center" style="writing-mode: vertical-rl; transform: rotate(180deg); max-width: 30px;">{{ j+1 }}</th>
                                                {% endfor %}
                                            </tr>
                                        </thead>
                                        <tbody>
                                            {% for i, row in enumerate(comp.stats.angle_similarity_matrix) %}
                                            <tr>
                                                <th>{{ i+1 }}</th>
                                                {% for val in row %}
                                                <td class="text-center">{{ "%.3f"|format(val) }}</td>
                                                {% endfor %}
                                            </tr>
                                            {% endfor %}
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Raw Cosine Similarity -->
                <h5>
                    {% if comp.stats.calibration_path %}
                    <span class="badge badge-secondary">Raw</span>
                    {% endif %}
                    Cosine Similarity
                </h5>
                <p class="text-muted small">Raw cosine similarity between embedding vectors. Not a proper metric (doesn't satisfy triangle inequality).
                {% if comp.stats.calibration_path %}<strong class="text-warning">These are RAW values before calibration.</strong>{% endif %}
                </p>
                <div class="row mb-4">
                    <div class="col-md-6">
                        <div class="card">
                            <div class="card-header bg-info text-white">
                                Continuous Values (Raw)
                            </div>
                            <div class="card-body">
                                <img class="img-fluid img-thumbnail lightbox-trigger" src="data:image/png;base64,{{ comp.plots.heatmaps_cosine.base64 }}" alt="Cosine similarity heatmap">
                            </div>
                        </div>
                    </div>
                    <div class="col-md-6">
                        <!-- Raw cosine matrix as table -->
                        <div class="card h-100">
                            <div class="card-header bg-secondary text-white">
                                <a class="text-white collapse-toggle" data-toggle="collapse" href="#cosine-matrix-table-{{ pair_idx }}">
                                    <span class="arrow">▶</span> Raw Cosine Matrix (Table)
                                </a>
                            </div>
                            <div class="collapse" id="cosine-matrix-table-{{ pair_idx }}">
                                <div class="card-body table-responsive">
                                    <table class="table table-sm table-bordered" style="font-size: 0.75rem;">
                                        <thead>
                                            <tr>
                                                <th></th>
                                                {% for j in range(comp.stats.similarity_matrix[0]|length) %}
                                                <th class="text-center" style="writing-mode: vertical-rl; transform: rotate(180deg); max-width: 30px;">{{ j+1 }}</th>
                                                {% endfor %}
                                            </tr>
                                        </thead>
                                        <tbody>
                                            {% for i, row in enumerate(comp.stats.similarity_matrix) %}
                                            <tr>
                                                <th>{{ i+1 }}</th>
                                                {% for val in row %}
                                                <td class="text-center">{{ "%.3f"|format(val) }}</td>
                                                {% endfor %}
                                            </tr>
                                            {% endfor %}
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Additional Distance Metrics -->
                <h5>Alternative Distance Metrics</h5>
                <div class="row">
                    <!-- Shepard Similarity -->
                    <div class="col-md-4 mb-3">
                        <div class="card h-100">
                            <div class="card-header bg-secondary text-white">
                                <h6 class="mb-0">Shepard Similarity <a href="#note-shepard" class="note-link text-white" title="About Shepard similarity">?</a></h6>
                            </div>
                            <div class="card-body">
                                <p class="small text-muted">Exponential decay on angular distance (k={{ comp.stats.shepard_k_value }}). Cognitively realistic similarity function.</p>
                                <img class="img-fluid img-thumbnail lightbox-trigger" src="data:image/png;base64,{{ comp.plots.heatmaps_shepard.base64 }}" alt="Shepard similarity heatmap">
                            </div>
                        </div>
                    </div>

                </div>


            </div><!-- end pair-pane -->
            {% endfor %}
            <!-- End of pair panes -->

        <hr class="mt-5">

        <!-- ============================================== -->
        <!-- THEMES & EMBEDDINGS SECTION (Reference)        -->
        <!-- ============================================== -->
        <div id="themes-embeddings" class="mt-4">
            <h3>
                <a class="collapse-toggle text-dark" data-toggle="collapse" href="#themes-embeddings-content" role="button" aria-expanded="false">
                    <span class="arrow">▶</span> Themes &amp; Embeddings
                </a>
                <small class="text-muted ml-2">Full text of embedded themes for each analysis</small>
            </h3>
            <div class="collapse" id="themes-embeddings-content">
                <div class="card card-body">
                    <p class="text-muted">The actual strings that were embedded for similarity comparison. Labels are used in plots; embedded strings are used for calculating similarity.</p>

                    {% for analysis_name, themes in comparison.embedded_strings.items() %}
                    <div class="card mb-4">
                        <div class="card-header">
                            <strong>{{ analysis_name }}</strong> -- Embedded Themes ({{ themes | length }})
                        </div>
                        <div class="card-body">
                            <div class="table-responsive">
                                <table class="table table-sm table-striped">
                                    <thead>
                                        <tr>
                                            <th style="width: 5%;">ID</th>
                                            <th style="width: 20%;">Theme Name</th>
                                            <th style="width: 15%;">Short Label</th>
                                            <th style="width: 60%;">Embedded String</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        {% for item in themes %}
                                        <tr id="theme-{{ analysis_name | replace(' ', '-') }}-{{ item.theme_index }}">
                                            <td class="small text-muted">{{ item.set_letter }}{{ item.theme_index }}</td>
                                            <td><span class="small"><b>{{ item.theme_name }}</b></span></td>
                                            <td><span class="small text-info">{{ item.set_letter }}{{ item.theme_index }}: {{ item.short_label | default('Theme ' ~ item.theme_index) }}</span></td>
                                            <td><span class="small text-muted">{{ item.embedded_string }}</span></td>
                                        </tr>
                                        {% endfor %}
                                    </tbody>
                                </table>
                            </div>
                        </div>
                    </div>
                    {% endfor %}
                </div>
            </div>
        </div>

        <hr class="mt-5">

        <!-- ============================================== -->
        <!-- SYNTHETIC BASELINES SECTION                    -->
        <!-- ============================================== -->
        <div id="baselines" class="mt-4">
            <h3>
                <a class="collapse-toggle text-dark" data-toggle="collapse" href="#baselines-content" role="button" aria-expanded="false">
                    <span class="arrow">▶</span> Synthetic Baselines
                </a>
                <small class="text-muted ml-2">Paraphrase ceiling and word-salad floor calibration data</small>
            </h3>
            <div class="collapse" id="baselines-content">
                <div class="card card-body">
                    <p class="text-muted">Synthetic baselines help calibrate alignment scores by providing reference points for "best case" (paraphrase ceiling) and "worst case" (word-salad floor) scenarios.</p>

                    {% for key, comp in comparison.by_comparisons().items() %}
                    <div class="mb-4">
                        <h5>{{ comp.a.name }} vs {{ comp.b.name }}</h5>

                        {% if comp.stats.paraphrase_baseline %}
                        <!-- Paraphrase Ceiling -->
                        <div class="card mb-3">
                            <div class="card-header bg-success text-white">
                                <strong>Paraphrase Ceiling</strong> -- Best-case reference
                            </div>
                            <div class="card-body">
                                <p class="small text-muted">LLM-generated paraphrases of each theme establish what alignment looks like when themes have identical meaning but different wording. This represents the best achievable similarity between semantically equivalent analyses.</p>
                                <div class="row mb-3">
                                    <div class="col-md-6">
                                        <ul class="list-unstyled small">
                                            <li><strong>Mean self-similarity:</strong> {{ "%.3f"|format(comp.stats.paraphrase_baseline.paraphrase_similarity_mean) if comp.stats.paraphrase_baseline.paraphrase_similarity_mean is not none else "N/A" }}</li>
                                            <li><strong>Model:</strong> {{ comp.stats.paraphrase_baseline.metadata.model }}</li>
                                            <li><strong>Paraphrases per theme:</strong> {{ comp.stats.paraphrase_baseline.metadata.n_paraphrases }}</li>
                                        </ul>
                                    </div>
                                </div>

                                <!-- Paraphrase samples (collapsed) -->
                                <a class="collapse-toggle small" data-toggle="collapse" href="#para-samples-{{ loop.index }}" role="button" aria-expanded="false">
                                    <span class="arrow">▶</span> View sample paraphrases
                                </a>
                                <div class="collapse" id="para-samples-{{ loop.index }}">
                                    <div class="row mt-2">
                                        <div class="col-md-6">
                                            <h6 class="small text-muted">{{ comp.a.name }} samples:</h6>
                                            {% for sample in comp.stats.paraphrase_baseline.samples_a[:3] %}
                                            <div class="card mb-2">
                                                <div class="card-body py-2">
                                                    <strong class="small">Original:</strong>
                                                    <div class="text-monospace small">{{ sample.original[:100] }}{% if sample.original|length > 100 %}...{% endif %}</div>
                                                    <strong class="small text-muted">Paraphrases:</strong>
                                                    {% for para in sample.paraphrases[:2] %}
                                                    <div class="text-monospace small text-muted">• {{ para[:80] }}{% if para|length > 80 %}...{% endif %}</div>
                                                    {% endfor %}
                                                </div>
                                            </div>
                                            {% endfor %}
                                        </div>
                                        <div class="col-md-6">
                                            <h6 class="small text-muted">{{ comp.b.name }} samples:</h6>
                                            {% for sample in comp.stats.paraphrase_baseline.samples_b[:3] %}
                                            <div class="card mb-2">
                                                <div class="card-body py-2">
                                                    <strong class="small">Original:</strong>
                                                    <div class="text-monospace small">{{ sample.original[:100] }}{% if sample.original|length > 100 %}...{% endif %}</div>
                                                    <strong class="small text-muted">Paraphrases:</strong>
                                                    {% for para in sample.paraphrases[:2] %}
                                                    <div class="text-monospace small text-muted">• {{ para[:80] }}{% if para|length > 80 %}...{% endif %}</div>
                                                    {% endfor %}
                                                </div>
                                            </div>
                                            {% endfor %}
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                        {% endif %}

                        {% if comp.stats.word_salad_samples %}
                        <!-- Word Salad Floor -->
                        <div class="card mb-3">
                            <div class="card-header bg-danger text-white">
                                <strong>Word Salad Floor</strong> -- Random baseline
                            </div>
                            <div class="card-body">
                                <p class="small text-muted">Word salad is generated by randomly shuffling words from themes, destroying semantic meaning. This represents what you'd expect from random text with similar vocabulary -- a floor below which alignment cannot meaningfully fall.</p>
                                <ul class="list-unstyled small">
                                    <li><strong>Samples generated:</strong> {{ comp.stats.word_salad_samples|length }}</li>
                                    <li><strong>Method:</strong> Words randomly shuffled while preserving theme length</li>
                                    {% if comp.stats.word_salad_self_similarity_raw is not none %}
                                    <li><strong>Self-similarity:</strong> {{ "%.3f"|format(comp.stats.word_salad_self_similarity_raw) }} (rescaled: {{ "%.3f"|format(comp.stats.word_salad_self_similarity) }})</li>
                                    {% endif %}
                                </ul>

                                <!-- Word salad samples (collapsed) -->
                                <a class="collapse-toggle small" data-toggle="collapse" href="#wordsalad-samples-{{ loop.index }}" role="button" aria-expanded="false">
                                    <span class="arrow">▶</span> View word salad samples
                                </a>
                                <div class="collapse" id="wordsalad-samples-{{ loop.index }}">
                                    <div class="mt-2" style="max-height: 300px; overflow-y: auto;">
                                        {% for sample in comp.stats.word_salad_samples[:3] %}
                                        <div class="card mb-2">
                                            <div class="card-header py-1 bg-light small">Sample {{ loop.index }}</div>
                                            <div class="card-body py-2">
                                                {% for text in sample[:5] %}
                                                <div class="text-monospace small text-muted">{{ loop.index }}. {{ text[:100] }}{% if text|length > 100 %}...{% endif %}</div>
                                                {% endfor %}
                                                {% if sample|length > 5 %}<div class="small text-muted">... and {{ sample|length - 5 }} more</div>{% endif %}
                                            </div>
                                        </div>
                                        {% endfor %}
                                    </div>
                                </div>
                            </div>
                        </div>
                        {% endif %}

                        {% if comp.stats.color_scale_info %}
                        <!-- Color Scale Info -->
                        <div class="card mb-3">
                            <div class="card-header bg-secondary text-white">
                                <strong>Colour Scale</strong> -- Sankey diagram interpretation
                            </div>
                            <div class="card-body">
                                <p class="small text-muted">{{ comp.stats.color_scale_info.description }}</p>
                                <ul class="list-unstyled small">
                                    <li><span class="badge badge-success">Green</span> Strong match (low cost, well below K threshold)</li>
                                    <li><span class="badge badge-warning">Amber</span> Moderate match</li>
                                    <li><span class="badge badge-danger">Red</span> Marginal match (cost approaching K, close to being dropped)</li>
                                </ul>
                                <p class="small text-muted mb-0">Baselines: word-salad = {{ "%.3f"|format(comp.stats.color_scale_info.word_salad_self_similarity) }}, paraphrase = {{ "%.3f"|format(comp.stats.color_scale_info.paraphrase_self_similarity) }}</p>
                            </div>
                        </div>
                        {% endif %}
                    </div>
                    {% endfor %}
                </div>
            </div>
        </div>

        <hr class="mt-5">

        <!-- ============================================== -->
        <!-- NOTES & EXPLANATIONS SECTION                   -->
        <!-- ============================================== -->
        <div id="notes" class="notes-section mt-4">
            <h3>Notes &amp; Explanations</h3>
            <p class="text-muted">Reference guide for interpreting the metrics and visualisations in this report.</p>

            <h5 id="note-umap">UMAP Interpretation</h5>
            <p class="small">UMAP is a non-linear dimensionality-reduction method that prioritises preserving local neighbourhood structure rather than global variance. Nearby points can be interpreted as closely related themes, while larger-scale distances and cluster shapes should be interpreted qualitatively rather than metrically. This plot is intended as an exploratory visualisation of thematic relationships, not as a quantitative evaluation.</p>

            <h5 id="note-angular-distance">Angular Distance</h5>
            <p class="small">Angular distance uses the angle between embedding vectors (arccos of cosine), normalised to [0,1]. Unlike cosine similarity, it satisfies the triangle inequality, making it a proper metric for averaging and comparison. A value of 1.0 indicates identical vectors; 0.0 indicates orthogonal vectors.</p>

            <h5 id="note-optimal-transport">Optimal Transport</h5>
            <p class="small">Unbalanced Optimal Transport (OT) finds the most efficient way to "transport" mass from one theme set to another while allowing some mass to remain unmatched. Unlike standard matching algorithms, OT can express many-to-many relationships -- a single theme can contribute to multiple themes in the other set, and vice versa. This is appropriate when themes may be split, merged, or refined across analyses.</p>

            <h5 id="note-k-parameter">K Parameter</h5>
            <p class="small">The K parameter (reg_m) controls the penalty for leaving mass unmatched in optimal transport. Lower K = more selective matching (themes must be very similar to match). Higher K = more permissive matching (more mass is transported). The scree plots help identify the elbow where increasing K provides diminishing returns.</p>

            <h5 id="note-shared-mass">Shared Mass</h5>
            <p class="small">Shared mass is the proportion of total thematic content that was matched between the two sets. A value of 100% means all themes found partners; lower values indicate some themes were left unmatched (novel or missing concepts).</p>

            <h5 id="note-alignment">Semantic Alignment</h5>
            <p class="small">Alignment measures the quality of theme-to-theme matches (computed as 1 - transport cost). Higher alignment means matched themes are more semantically similar. This is the "how good" complement to shared mass's "how much".</p>

            <h5 id="note-coverage-hitrates">Coverage &amp; Hit Rates</h5>
            <p class="small">Coverage measures what proportion of themes found at least one match above the similarity threshold. Hit Rate A = proportion of A themes with matches; Hit Rate B = proportion of B themes with matches. High hit rates indicate both analyses found similar conceptual territory.</p>

            <h5 id="note-fidelity">Fidelity</h5>
            <p class="small">Fidelity is the harmonic mean of directional best-match scores (A→B and B→A). It measures "how close are the best matches on average?" Higher fidelity = tighter semantic alignment between theme sets.</p>

            <h5 id="note-hungarian">Hungarian Algorithm</h5>
            <p class="small">The Hungarian algorithm finds the optimal one-to-one pairing that maximises total similarity. Each theme maps to at most one partner -- no reuse allowed. This removes ambiguity but penalises legitimate theme refinement (splitting one theme into two appears as unmatched).</p>

            <h5 id="note-paraphrase-ceiling">Paraphrase Ceiling</h5>
            <p class="small">LLM-generated paraphrases of each theme establish what alignment looks like when themes have identical meaning but different wording. If observed alignment reaches this level, the analyses are semantically equivalent.</p>

            <h5 id="note-wordsalad-floor">Word Salad Floor</h5>
            <p class="small">Word salad is generated by randomly shuffling words from themes, destroying semantic meaning. This represents what you'd expect from random text with similar vocabulary. The further above this baseline, the more genuine the semantic similarity.</p>

            <h5 id="note-theme-preferences">Theme Preference Structure</h5>
            <p class="small">This measures whether themes have <strong>preferred partners</strong> that enable meaningful alignment, or are largely interchangeable (diffuse overlap).</p>
            <ul class="small">
                <li><strong>Relative preference:</strong> How much better is each theme's best match compared to its average match, as a percentage of mean cost. Higher values (>15%) indicate themes have clear preferred partners.</li>
                <li><strong>Preference strength:</strong> The absolute difference between best and average match costs. Larger values mean stronger preferences.</li>
                <li><strong>Spikiness:</strong> How varied the cost matrix is (std/range). Higher values (>0.2) mean some pairs are much more similar than others.</li>
            </ul>
            <p class="small text-muted">When preferences are weak (<8%), the two analyses describe overlapping conceptual territory but themes don't map neatly onto each other -- they're interchangeable from the OT perspective. This isn't necessarily a problem; it characterises the relationship as diffuse overlap rather than one-to-one correspondence.</p>

            <h5 id="note-effect-size">Effect Size (MADs)</h5>
            <p class="small">Effect size shows how many MADs (median absolute deviations) above the word-salad baseline the observed value falls. This measures how distinct the observed alignment is from random text -- higher values indicate more meaningful semantic similarity. MAD is a robust measure less sensitive to outliers than standard deviation.</p>

            <h5 id="note-sankey-colours">Sankey Colour Scale</h5>
            <p class="small">The Sankey diagram supports two colour modes, selectable via the toggle button:</p>

            <p class="small"><strong>1. Similarity-based (default):</strong> Uses absolute similarity thresholds for consistent interpretation across different K values.</p>
            <ul class="small">
                <li><strong>Green:</strong> similarity >= {{ comparison.config.color_green | default(0.75) }} (strong semantic match)</li>
                <li><strong>Amber:</strong> moderate similarity</li>
                <li><strong>Red:</strong> similarity <= {{ comparison.config.color_red | default(0.4) }} (weak match)</li>
            </ul>

            <p class="small"><strong>2. K-relative:</strong> Shows how close each match is to being dropped at the current K threshold.</p>
            <ul class="small">
                <li><strong>Green:</strong> cost &lt; {{ comparison.config.k_color_green | default(0.2) }}×K (strong match, well below threshold)</li>
                <li><strong>Amber:</strong> moderate cost</li>
                <li><strong>Red:</strong> cost &gt; {{ comparison.config.k_color_red | default(1.1) }}×K (marginal match, close to being dropped)</li>
            </ul>
            <p class="small text-muted">In K-relative mode, the scale adapts to each K value. Links with cost near K appear red because the OT algorithm is nearly indifferent between keeping them or destroying the mass. This mode is useful for understanding the sensitivity of matches to the K parameter.</p>

            <h5 id="note-transport-heatmap">Transport Heatmap</h5>
            <p class="small">The transport heatmap shows how mass flows from A themes (rows) to B themes (columns). Cell values show percentage of total transported mass. Darker cells indicate stronger transport links.</p>

            <h5 id="note-splits-joins">Splits &amp; Joins</h5>
            <p class="small">Splits measure how many B themes each A theme connects to (1.0 = perfect 1:1). Joins measure how many A themes each B theme receives from. Higher values indicate more many-to-many relationships in the transport plan.</p>

            <h5 id="note-shepard">Shepard Similarity</h5>
            <p class="small">Shepard similarity applies exponential decay to angular distance, providing a cognitively realistic similarity function where small distance differences matter more for nearby items than for distant ones.</p>
        </div>

        <hr class="mt-5">

        <div id="config" class="comparison-config mt-4">
            <h3>Configuration</h3>
            <pre class="bg-light p-3" style="white-space: pre-wrap; font-size: 0.75rem;">{{ comparison.config | tojson(indent=2) }}</pre>
        </div>

        {% if calibration_info %}
        <hr class="mt-5">

        <div id="calibration" class="calibration-info mt-4">
            <h3>Calibration Model</h3>
            <p class="text-muted">Similarity scores are transformed using a calibration model trained on paraphrases of varying semantic similarity.</p>

            <div class="row">
                <div class="col-md-6">
                    <h5>Training Information</h5>
                    <table class="table table-sm">
                        <tbody>
                            <tr>
                                <td><strong>Embedding Model</strong></td>
                                <td><code>{{ calibration_info.metadata.embedding_model }}</code></td>
                            </tr>
                            <tr>
                                <td><strong>Template</strong></td>
                                <td><code>{{ calibration_info.metadata.embedding_template }}</code></td>
                            </tr>
                            {% if calibration_info.metadata.validation %}
                            <tr>
                                <td><strong>Training Samples</strong></td>
                                <td>{{ calibration_info.metadata.validation.n_train }}</td>
                            </tr>
                            <tr>
                                <td><strong>Test Samples</strong></td>
                                <td>{{ calibration_info.metadata.validation.n_test }}</td>
                            </tr>
                            <tr>
                                <td><strong>Category Accuracy</strong></td>
                                <td>{{ "%.1f" | format(calibration_info.metadata.validation.category_accuracy * 100) }}%</td>
                            </tr>
                            {% endif %}
                        </tbody>
                    </table>

                    <h5>Target Values</h5>
                    <table class="table table-sm">
                        <thead>
                            <tr>
                                <th>Category</th>
                                <th>Target</th>
                                <th>Raw Mean</th>
                                <th>Raw Range</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for cat, target in calibration_info.metadata.targets.items() %}
                            <tr>
                                <td>{{ cat }}</td>
                                <td>{{ "%.2f" | format(target) }}</td>
                                {% if calibration_info.metadata.category_stats[cat] %}
                                <td>{{ "%.3f" | format(calibration_info.metadata.category_stats[cat].mean) }}</td>
                                <td>{{ "%.3f" | format(calibration_info.metadata.category_stats[cat].min) }} - {{ "%.3f" | format(calibration_info.metadata.category_stats[cat].max) }}</td>
                                {% else %}
                                <td>-</td>
                                <td>-</td>
                                {% endif %}
                            </tr>
                            {% endfor %}
                        </tbody>
                    </table>
                </div>

                <div class="col-md-6">
                    <h5>Calibration Curve</h5>
                    <p class="small text-muted">Raw angular similarity (x-axis) mapped to calibrated semantic similarity (y-axis). Coloured points show training data for each paraphrase category.</p>

                    {% if calibration_info.plot_base64 %}
                    <img src="data:image/png;base64,{{ calibration_info.plot_base64 }}"
                         alt="Calibration curve"
                         style="max-width: 100%; height: auto; border-radius: 4px;"
                         class="img-fluid mb-3">
                    {% else %}
                    <svg viewBox="0 0 300 220" style="max-width: 100%; height: auto; background: #f8f9fa; border-radius: 4px;">
                        <!-- axes -->
                        <line x1="40" y1="180" x2="280" y2="180" stroke="#333" stroke-width="1"/>
                        <line x1="40" y1="180" x2="40" y2="20" stroke="#333" stroke-width="1"/>

                        <!-- axis labels -->
                        <text x="160" y="210" text-anchor="middle" font-size="10" fill="#666">Raw Similarity</text>
                        <text x="15" y="100" text-anchor="middle" font-size="10" fill="#666" transform="rotate(-90 15 100)">Calibrated</text>

                        <!-- tick marks and labels -->
                        <text x="40" y="195" text-anchor="middle" font-size="8" fill="#666">0.5</text>
                        <text x="160" y="195" text-anchor="middle" font-size="8" fill="#666">0.75</text>
                        <text x="280" y="195" text-anchor="middle" font-size="8" fill="#666">1.0</text>
                        <text x="35" y="180" text-anchor="end" font-size="8" fill="#666">0</text>
                        <text x="35" y="100" text-anchor="end" font-size="8" fill="#666">0.5</text>
                        <text x="35" y="20" text-anchor="end" font-size="8" fill="#666">1.0</text>

                        <!-- grid lines -->
                        <line x1="40" y1="100" x2="280" y2="100" stroke="#ddd" stroke-width="0.5" stroke-dasharray="4"/>
                        <line x1="160" y1="20" x2="160" y2="180" stroke="#ddd" stroke-width="0.5" stroke-dasharray="4"/>

                        <!-- diagonal reference line (identity) -->
                        <line x1="40" y1="180" x2="280" y2="20" stroke="#ccc" stroke-width="1" stroke-dasharray="4"/>

                        <!-- calibration curve -->
                        <polyline fill="none" stroke="#0d6efd" stroke-width="2" points="
                            {% for pt in calibration_info.transformation %}{{ 40 + (pt.raw - 0.5) * 480 }},{{ 180 - pt.calibrated * 160 }} {% endfor %}
                        "/>

                        <!-- data points -->
                        {% for pt in calibration_info.transformation %}
                        <circle cx="{{ 40 + (pt.raw - 0.5) * 480 }}" cy="{{ 180 - pt.calibrated * 160 }}" r="2" fill="#0d6efd"/>
                        {% endfor %}
                    </svg>
                    {% endif %}

                    <h5 class="mt-3">Sample Transformations</h5>
                    <table class="table table-sm">
                        <thead>
                            <tr>
                                <th>Raw</th>
                                <th>Calibrated</th>
                            </tr>
                        </thead>
                        <tbody>
                            {% for pt in calibration_info.transformation %}
                            {% if loop.index0 % 4 == 0 %}
                            <tr>
                                <td>{{ "%.3f" | format(pt.raw) }}</td>
                                <td>{{ "%.3f" | format(pt.calibrated) }}</td>
                            </tr>
                            {% endif %}
                            {% endfor %}
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
        {% endif %}

        <hr class="mt-5">

        <div id="additional-data" class="additional-data mt-4">
            
        </div>

        <footer class="screen-footer mt-4 pt-3 border-top text-muted small text-center">
            <p class="mb-1">Made with <strong>soak</strong> v{{ soak_version | default('unknown') }}</p>
            <p class="mb-0"><a href="https://github.com/benwhalley/soak" class="text-muted">github.com/benwhalley/soak</a></p>
        </footer>
    </div>

    <!-- Lightbox overlay for enlarged images -->
    <div class="lightbox-overlay" id="lightbox">
        <span class="lightbox-close">&times;</span>
        <img src="" alt="Enlarged view" id="lightbox-img">
    </div>

    <!-- Info Panel (offcanvas) for theme details and notes -->
    <div class="info-panel-overlay" id="info-panel-overlay"></div>
    <div class="info-panel" id="info-panel">
        <div class="info-panel-header">
            <h5 id="info-panel-title">Details</h5>
            <button class="info-panel-close" id="info-panel-close" aria-label="Close">&times;</button>
        </div>
        <div class="info-panel-body" id="info-panel-body">
            <!-- Content dynamically populated -->
        </div>
    </div>

    <script>
    // Lightbox functionality
    (function() {
        const lightbox = document.getElementById('lightbox');
        const lightboxImg = document.getElementById('lightbox-img');
        const closeBtn = lightbox.querySelector('.lightbox-close');

        // Open lightbox when clicking on trigger images
        document.querySelectorAll('.lightbox-trigger').forEach(function(img) {
            img.addEventListener('click', function() {
                lightboxImg.src = this.src;
                lightbox.classList.add('active');
                document.body.style.overflow = 'hidden';
            });
        });

        // Close lightbox when clicking overlay or close button
        lightbox.addEventListener('click', function(e) {
            if (e.target === lightbox || e.target === closeBtn) {
                lightbox.classList.remove('active');
                document.body.style.overflow = '';
            }
        });

        // Close on Escape key
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape' && lightbox.classList.contains('active')) {
                lightbox.classList.remove('active');
                document.body.style.overflow = '';
            }
        });
    })();

    // Initialize Bootstrap tooltips
    $(function () {
        $('[data-toggle="tooltip"]').tooltip();
    });

    // Handle theme label anchor links - expand collapsed section if needed
    document.querySelectorAll('.theme-label-link').forEach(function(link) {
        link.addEventListener('click', function(e) {
            e.preventDefault();
            var targetId = this.getAttribute('href').substring(1);
            var target = document.getElementById(targetId);
            if (target) {
                // Expand the themes-embeddings section if collapsed
                var collapse = document.getElementById('themes-embeddings-content');
                if (collapse && !collapse.classList.contains('show')) {
                    $(collapse).collapse('show');
                    // Wait for collapse animation then scroll
                    setTimeout(function() {
                        target.scrollIntoView({behavior: 'smooth', block: 'center'});
                        target.classList.add('table-warning');
                        setTimeout(function() { target.classList.remove('table-warning'); }, 2000);
                    }, 400);
                } else {
                    target.scrollIntoView({behavior: 'smooth', block: 'center'});
                    target.classList.add('table-warning');
                    setTimeout(function() { target.classList.remove('table-warning'); }, 2000);
                }
            }
        });
    });

    // Initialise nouislider for K-value selection with cross-pair synchronisation
    (function() {
        // Global K value - shared across all pairs
        var globalSelectedK = null;
        var sliders = {};
        var pairKValues = {};
        var isSyncing = false; // Guard against re-entrancy

        // Store K values for each pair (rounded to 3dp for consistency)
        {% for key, comp in comparison.by_comparisons().items() %}
        {% set pair_idx = loop.index %}
        pairKValues[{{ pair_idx }}] = [{% for k in comp.stats.k_values if k >= 0.1 %}{{ "%.3f"|format(k) }}{% if not loop.last %}, {% endif %}{% endfor %}];
        {% endfor %}

        // Find closest available K value for a pair
        function findClosestK(pairIdx, targetK) {
            var kValues = pairKValues[pairIdx];
            if (!kValues || kValues.length === 0) return targetK;
            var closest = kValues[0];
            var minDiff = Math.abs(kValues[0] - targetK);
            kValues.forEach(function(k) {
                var diff = Math.abs(k - targetK);
                if (diff < minDiff) {
                    minDiff = diff;
                    closest = k;
                }
            });
            return closest;
        }

        // Update content panes for a specific pair
        function updateContentPanes(pairIdx, kVal) {
            var kValues = pairKValues[pairIdx];
            if (!kValues) return;

            // Update display badge
            var display = document.getElementById('k-value-display-' + pairIdx);
            if (display) display.textContent = kVal.toFixed(3);

            // Show/hide content panes
            kValues.forEach(function(k) {
                // Use toFixed(3) for consistent ID matching with template-generated IDs
                var kId = k.toFixed(3).replace('.', '-');
                var contentPane = document.getElementById('k-content-' + pairIdx + '-' + kId);
                var isActive = Math.abs(k - kVal) < 0.0001;
                if (contentPane) {
                    contentPane.style.display = isActive ? 'block' : 'none';
                }
            });

            // Preserve Sankey color mode for newly visible pane
            if (typeof applySankeyColorMode === 'function') {
                applySankeyColorMode();
            }
        }

        // Sync all pairs to a given K value
        function syncAllPairsToK(sourceK, sourcePairIdx) {
            if (isSyncing) return; // Prevent re-entrancy
            isSyncing = true;
            globalSelectedK = sourceK;

            // Update all pairs dynamically
            Object.keys(pairKValues).forEach(function(key) {
                var pairIdx = parseInt(key, 10);
                if (pairIdx === sourcePairIdx) return; // Skip source pair

                var closestK = findClosestK(pairIdx, sourceK);
                updateContentPanes(pairIdx, closestK);

                // Update slider position silently (don't trigger update event)
                var slider = sliders[pairIdx];
                if (slider && slider.noUiSlider) {
                    slider.noUiSlider.set(closestK, false); // false = don't fire events
                }
            });

            isSyncing = false;
        }

        // Create update handler for a pair
        function createUpdateHandler(pairIdx) {
            return function(values, handle) {
                if (isSyncing) return; // Ignore updates during sync
                var rawVal = parseFloat(values[handle]);
                // Snap to nearest exact K value to avoid floating point errors
                var kVal = findClosestK(pairIdx, rawVal);
                updateContentPanes(pairIdx, kVal);
                syncAllPairsToK(kVal, pairIdx);
            };
        }

        {% for key, comp in comparison.by_comparisons().items() %}
        {% set pair_idx = loop.index %}
        (function() {
            var pairIdx = {{ pair_idx }};
            var kValues = pairKValues[pairIdx];
            var defaultK = {{ "%.3f"|format(comp.stats.default_k) }};
            var chordK = {{ "%.3f"|format(comp.stats.chord_k) }};
            var diminishingK = {{ "%.3f"|format(comp.stats.diminishing_k) }};

            var slider = document.getElementById('k-slider-' + pairIdx);
            if (slider && typeof noUiSlider !== 'undefined') {
                sliders[pairIdx] = slider;

                // Find index of diminishing K (default selection)
                var closestDiminishing = findClosestK(pairIdx, diminishingK);
                var defaultIndex = kValues.indexOf(closestDiminishing);
                if (defaultIndex < 0) defaultIndex = 0;

                // Set initial global K if not set
                if (globalSelectedK === null) {
                    globalSelectedK = kValues[defaultIndex];
                }

                noUiSlider.create(slider, {
                    start: [kValues[defaultIndex]],
                    snap: true,
                    range: (function() {
                        var range = {'min': kValues[0], 'max': kValues[kValues.length - 1]};
                        kValues.forEach(function(k, i) {
                            if (i > 0 && i < kValues.length - 1) {
                                var pct = (i / (kValues.length - 1)) * 100;
                                range[pct + '%'] = k;
                            }
                        });
                        return range;
                    })(),
                    pips: {
                        mode: 'steps',
                        filter: function(value, type) {
                            // Large pip (with label) for endpoints, small pip for others
                            if (Math.abs(value - kValues[0]) < 0.001 || Math.abs(value - kValues[kValues.length-1]) < 0.001) {
                                return 1; // large
                            }
                            return 0; // small (no label)
                        },
                        format: {
                            to: function(value) { return value.toFixed(3); }
                        }
                    }
                });

                // Add special markers for important K values
                setTimeout(function() {
                    slider.querySelectorAll('.noUi-marker').forEach(function(marker) {
                        var pct = parseFloat(marker.style.left);
                        var idx = Math.round(pct / 100 * (kValues.length - 1));
                        var val = kValues[idx];
                        if (val !== undefined) {
                            if (Math.abs(val - defaultK) < 0.001) marker.classList.add('k-marker-default');
                            if (Math.abs(val - chordK) < 0.001) marker.classList.add('k-marker-elbow');
                            if (Math.abs(val - diminishingK) < 0.001) marker.classList.add('k-marker-diminishing');
                        }
                    });
                }, 50);

                // Update display and show/hide content panes, sync across pairs
                slider.noUiSlider.on('update', createUpdateHandler(pairIdx));
            }
        })();
        {% endfor %}

        // When switching pairs, sync to current global K
        var originalSwitchPair = window.switchPair;
        window.switchPair = function(index) {
            originalSwitchPair(index);

            // After switching, ensure the new pair shows the global K
            if (globalSelectedK !== null) {
                var closestK = findClosestK(index, globalSelectedK);
                var slider = sliders[index];
                if (slider && slider.noUiSlider) {
                    slider.noUiSlider.set(closestK);
                }
                updateContentPanes(index, closestK);
            }
        };
    })();

    // CSV copy function for matrices
    function copyMatrixCSV(matrixId) {
        // Placeholder - would extract data from the corresponding similarity matrix
        alert('CSV copy functionality coming soon');
    }

    // Sankey colour mode toggle (with persistence across K changes)
    var sankeyColorMode = 'sim'; // Global state: 'sim' or 'k-rel'

    function applySankeyColorMode(mode) {
        if (mode) sankeyColorMode = mode;
        // Apply to all Sankey cards
        document.querySelectorAll('.sankey-color-toggle').forEach(function(btnGroup) {
            var card = btnGroup.closest('.card');
            var simFrame = card.querySelector('.sankey-frame[data-mode="sim"]');
            var kRelFrame = card.querySelector('.sankey-frame[data-mode="k-rel"]');

            // Update button states
            btnGroup.querySelectorAll('button').forEach(function(b) {
                b.classList.toggle('active', b.getAttribute('data-mode') === sankeyColorMode);
            });

            // Show/hide the appropriate Sankey frame
            if (simFrame) simFrame.style.display = sankeyColorMode === 'sim' ? '' : 'none';
            if (kRelFrame) kRelFrame.style.display = sankeyColorMode === 'k-rel' ? '' : 'none';
        });
    }

    (function() {
        // Set up click handlers for toggle buttons
        document.querySelectorAll('.sankey-color-toggle button').forEach(function(btn) {
            btn.addEventListener('click', function() {
                var mode = this.getAttribute('data-mode');
                applySankeyColorMode(mode);
            });
        });
    })();

    // =============================================
    // INFO PANEL (offcanvas) FUNCTIONALITY
    // =============================================
    (function() {
        var panel = document.getElementById('info-panel');
        var overlay = document.getElementById('info-panel-overlay');
        var closeBtn = document.getElementById('info-panel-close');
        var title = document.getElementById('info-panel-title');
        var body = document.getElementById('info-panel-body');

        // Theme data for populating the panel
        var themeData = {
            {% for analysis_name, themes in comparison.embedded_strings.items() %}
            '{{ analysis_name | replace("'", "\\'") }}': {
                {% for item in themes %}
                {{ item.theme_index }}: {
                    'set_letter': '{{ item.set_letter }}',
                    'theme_index': {{ item.theme_index }},
                    'theme_name': {{ item.theme_name | tojson }},
                    'short_label': {{ (item.short_label | default('Theme ' ~ item.theme_index)) | tojson }},
                    'embedded_string': {{ item.embedded_string | tojson }}
                }{% if not loop.last %},{% endif %}
                {% endfor %}
            }{% if not loop.last %},{% endif %}
            {% endfor %}
        };

        // Note explanations content
        var noteContent = {};
        document.querySelectorAll('.notes-section h5[id^="note-"]').forEach(function(h5) {
            var noteId = h5.id;
            var content = [];
            var sibling = h5.nextElementSibling;
            while (sibling && sibling.tagName !== 'H5') {
                content.push(sibling.outerHTML);
                sibling = sibling.nextElementSibling;
            }
            noteContent[noteId] = {
                title: h5.textContent,
                content: content.join('')
            };
        });

        function openPanel() {
            panel.classList.add('active');
            overlay.classList.add('active');
            document.body.style.overflow = 'hidden';
        }

        function closePanel() {
            panel.classList.remove('active');
            overlay.classList.remove('active');
            document.body.style.overflow = '';
        }

        // Close handlers
        closeBtn.addEventListener('click', closePanel);
        overlay.addEventListener('click', closePanel);
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape' && panel.classList.contains('active')) {
                closePanel();
            }
        });

        // Show a single theme
        function showTheme(analysisName, themeIndex) {
            var analysis = themeData[analysisName];
            if (!analysis) return;
            var theme = analysis[themeIndex];
            if (!theme) return;

            title.textContent = 'Theme Details';
            body.innerHTML = renderThemeHtml(theme, analysisName);
            openPanel();
        }

        // Show a pair of matched themes
        function showThemePair(analysisA, indexA, analysisB, indexB, similarity) {
            var themeA = themeData[analysisA] && themeData[analysisA][indexA];
            var themeB = themeData[analysisB] && themeData[analysisB][indexB];

            if (!themeA || !themeB) return;

            title.textContent = 'Matched Themes';
            var html = '<div class="info-panel-section">';
            html += '<h6>Theme A</h6>';
            html += renderThemeHtml(themeA, analysisA);
            html += '</div>';
            html += '<div class="info-panel-section">';
            html += '<h6 class="mt-3">Theme B</h6>';
            html += renderThemeHtml(themeB, analysisB);
            html += '</div>';
            if (similarity !== undefined) {
                html += '<div class="info-panel-section">';
                html += '<h6>Match Info</h6>';
                html += '<div class="info-panel-content">';
                html += '<strong>Similarity:</strong> ' + similarity;
                html += '</div></div>';
            }
            body.innerHTML = html;
            openPanel();
        }

        // Show a note/explanation
        function showNote(noteId) {
            var note = noteContent[noteId];
            if (!note) return;

            title.textContent = note.title;
            body.innerHTML = '<div class="info-panel-content">' + note.content + '</div>';
            openPanel();
        }

        // Render theme HTML
        function renderThemeHtml(theme, analysisName) {
            var html = '<div class="info-panel-theme">';
            html += '<div class="d-flex align-items-center mb-2">';
            html += '<span class="badge badge-primary mr-2">' + theme.set_letter + theme.theme_index + '</span>';
            html += '<span class="badge badge-secondary">' + analysisName + '</span>';
            html += '</div>';
            html += '<div class="info-panel-theme-name">' + escapeHtml(theme.short_label) + '</div>';
            html += '<div class="text-muted small mb-2"><em>' + escapeHtml(theme.theme_name) + '</em></div>';
            html += '<div class="info-panel-embedded">' + escapeHtml(theme.embedded_string) + '</div>';
            html += '</div>';
            return html;
        }

        function escapeHtml(text) {
            var div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }

        // Expose functions globally
        window.infoPanel = {
            showTheme: showTheme,
            showThemePair: showThemePair,
            showNote: showNote,
            close: closePanel
        };

        // Handle clicks on note links
        document.addEventListener('click', function(e) {
            var noteLink = e.target.closest('a.note-link[href^="#note-"]');
            if (noteLink) {
                e.preventDefault();
                var noteId = noteLink.getAttribute('href').substring(1);
                showNote(noteId);
            }
        });

        // Handle clicks on theme links in match tables (those in table rows with both themes)
        document.addEventListener('click', function(e) {
            var themeLink = e.target.closest('.theme-label-link');
            if (!themeLink) return;

            // Check if this is in a match table row (has arrow indicator)
            var tr = themeLink.closest('tr');
            if (tr) {
                var links = tr.querySelectorAll('.theme-label-link');
                if (links.length === 2) {
                    // This is a matched pair row
                    e.preventDefault();

                    var linkA = links[0];
                    var linkB = links[1];

                    // Parse analysis name and theme index from href
                    // href format: #theme-AnalysisName-ThemeIndex
                    var parseThemeLink = function(link) {
                        var href = link.getAttribute('href');
                        var match = href.match(/#theme-(.+)-(\d+)$/);
                        if (match) {
                            return {
                                analysisName: match[1].replace(/-/g, ' '),
                                themeIndex: parseInt(match[2], 10)
                            };
                        }
                        return null;
                    };

                    var infoA = parseThemeLink(linkA);
                    var infoB = parseThemeLink(linkB);

                    if (infoA && infoB) {
                        // Get similarity from the row if present
                        var simCell = tr.querySelector('td:last-child');
                        var similarity = simCell ? simCell.textContent.trim() : undefined;
                        showThemePair(infoA.analysisName, infoA.themeIndex, infoB.analysisName, infoB.themeIndex, similarity);
                        return;
                    }
                }
            }

            // Single theme link (not in a match row)
            e.preventDefault();
            var href = themeLink.getAttribute('href');
            var match = href.match(/#theme-(.+)-(\d+)$/);
            if (match) {
                var analysisName = match[1].replace(/-/g, ' ');
                var themeIndex = parseInt(match[2], 10);
                showTheme(analysisName, themeIndex);
            }
        });
    })();
    </script>
</body>
</html>