{% extends "base.html" %}
{% from "components/help_macros.html" import tooltip, help_panel, help_step, help_tip %}

{% set active_page = 'journal-quality' %}

{% block title %}Journal Quality - Deep Research System{% endblock %}

{% block extra_head %}
<style>
    .ldr-journal-quality-container { max-width: 1200px; margin: 0 auto; padding: 2rem; padding-bottom: 120px; }
    .ldr-metrics-header { background: var(--gradient-bg, var(--bg-secondary)); padding: 1.5rem; border-radius: 0.75rem; margin-bottom: 1.5rem; border: 1px solid var(--border-color); }
    .ldr-metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; margin-bottom: 1.5rem; }
    .ldr-metric-card { background: var(--bg-secondary); border-radius: 0.75rem; padding: 1.5rem; box-shadow: var(--card-shadow); border: 1px solid var(--border-color); transition: transform 0.2s ease; }
    .ldr-metric-card:hover { transform: translateY(-2px); }
    .ldr-metric-card h3 { margin: 0 0 1rem 0; font-size: 1rem; color: var(--text-primary); }
    .ldr-overall-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 1rem; }
    .ldr-stat-item { text-align: center; background: var(--bg-tertiary); padding: 1rem; border-radius: 0.5rem; border: 1px solid var(--border-color); }
    .ldr-stat-value { font-size: 1.5rem; font-weight: 700; color: var(--accent-primary); display: block; }
    .ldr-stat-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-secondary); margin-top: 0.25rem; }
    .ldr-chart-container { position: relative; height: 300px; }
    .ldr-loading { text-align: center; padding: 2.5rem; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 0.75rem; }
    .ldr-error { background: rgba(250,92,124,0.1); color: var(--error-color); padding: 1rem; border-radius: 0.75rem; border: 1px solid rgba(250,92,124,0.2); }

    /* Quality badges */
    .ldr-quality-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 2rem; padding: 0.2rem 0.5rem; border-radius: 0.375rem; font-size: 0.8125rem; font-weight: 600; color: white; }
    .ldr-quality-dangerous { background: var(--error-color); }
    .ldr-quality-low { background: var(--warning-color); color: #1a1a2e; }
    .ldr-quality-moderate { background: var(--accent-tertiary, #40bfff); }
    .ldr-quality-strong { background: var(--success-color); }
    .ldr-quality-elite { background: var(--accent-primary); }
    .ldr-quality-unknown { background: var(--text-muted, #8a8aa0); }

    /* Quartile chips (Q1=green, Q2=blue, Q3=yellow, Q4=orange) */
    .ldr-quartile-chip { display: inline-flex; align-items: center; justify-content: center; min-width: 2rem; padding: 0.15rem 0.45rem; border-radius: 0.375rem; font-size: 0.75rem; font-weight: 600; }
    .ldr-quartile-q1 { background: var(--success-color); color: white; }
    .ldr-quartile-q2 { background: var(--accent-primary); color: white; }
    .ldr-quartile-q3 { background: var(--warning-color); color: #1a1a2e; }
    .ldr-quartile-q4 { background: rgba(250,140,80,0.85); color: #1a1a2e; }
    .ldr-quartile-unknown { color: var(--text-muted, #8a8aa0); font-weight: 400; }

    /* Source badges */
    .ldr-source-badge { display: inline-block; padding: 0.15rem 0.4rem; border-radius: 0.25rem; font-size: 0.75rem; font-weight: 500; }
    .ldr-source-openalex { background: rgba(124,92,252,0.15); color: var(--accent-primary); }
    .ldr-source-doaj { background: rgba(10,207,151,0.15); color: var(--success-color); }
    .ldr-source-llm { background: rgba(249,188,11,0.15); color: var(--warning-color); }
    .ldr-source-predatory { background: rgba(250,92,124,0.15); color: var(--error-color); }

    /* Trust icons */
    .ldr-trust-icons { display: flex; gap: 0.4rem; flex-wrap: wrap; }
    .ldr-trust-icon { font-size: 0.8rem; opacity: 0.9; }
    .ldr-trust-icon[title] { cursor: help; }

    /* Journal controls */
    .ldr-journal-controls { display: flex; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; align-items: center; }
    .ldr-journal-controls input,
    .ldr-journal-controls select { padding: 0.5rem 0.75rem; border: 1px solid var(--border-color); border-radius: 0.375rem; background: var(--bg-tertiary); color: var(--text-primary); font-size: 0.875rem; }
    .ldr-journal-controls input { flex: 1; min-width: 200px; }

    /* Table */
    .ldr-journal-table-wrapper { overflow-x: auto; }
    .ldr-journal-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
    .ldr-journal-table th { position: sticky; top: 0; z-index: 1; background: var(--bg-tertiary); padding: 0.75rem 0.5rem; text-align: left; cursor: pointer; user-select: none; border-bottom: 2px solid var(--border-color); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.7rem; white-space: nowrap; }
    .ldr-journal-table th:hover { color: var(--accent-primary); }
    .ldr-journal-table th.ldr-sorted-asc::after { content: ' ▲'; font-size: 0.6rem; }
    .ldr-journal-table th.ldr-sorted-desc::after { content: ' ▼'; font-size: 0.6rem; }
    .ldr-journal-table td { padding: 0.6rem 0.5rem; border-bottom: 1px solid var(--border-color); vertical-align: middle; }
    .ldr-journal-table tr:hover { background: var(--bg-tertiary); }
    .ldr-journal-table .ldr-row-predatory { background: rgba(250,92,124,0.05); }
    .ldr-journal-table .ldr-col-right { text-align: right; }
    .ldr-journal-name { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }

    /* Pagination */
    .ldr-pagination { display: flex; justify-content: center; gap: 0.5rem; margin-top: 1rem; align-items: center; }
    .ldr-pagination-btn { padding: 0.4rem 0.75rem; border: 1px solid var(--border-color); border-radius: 0.375rem; background: var(--bg-tertiary); color: var(--text-primary); cursor: pointer; font-size: 0.8rem; }
    .ldr-pagination-btn:hover { background: var(--accent-primary); color: white; }
    .ldr-pagination-btn.ldr-active { background: var(--accent-primary); color: white; }
    .ldr-pagination-btn:disabled { opacity: 0.4; cursor: default; }
    .ldr-table-info { text-align: center; font-size: 0.8rem; color: var(--text-secondary); margin-top: 0.5rem; }

    /* Empty state */
    .ldr-empty-state { text-align: center; padding: 3rem; }

    @media (max-width: 768px) {
        .ldr-journal-quality-container { padding: 1rem; padding-bottom: 120px; }
        .ldr-metrics-grid { grid-template-columns: 1fr; }
        .ldr-overall-stats { grid-template-columns: repeat(2, 1fr); }
        .ldr-journal-controls { flex-direction: column; }
        .ldr-journal-controls input, .ldr-journal-controls select { width: 100%; }
        .ldr-col-impact, .ldr-col-publisher { display: none; }
    }
    @media (max-width: 480px) {
        .ldr-overall-stats { grid-template-columns: 1fr; }
        .ldr-col-source, .ldr-col-type { display: none; }
    }
    /* Tabs */
    .ldr-tab-bar { display: flex; gap: 0; margin-bottom: 1.5rem; border-bottom: 2px solid var(--border-color); }
    .ldr-tab-btn { padding: 0.75rem 1.5rem; border: none; background: none; color: var(--text-secondary); font-size: 0.95rem; font-weight: 500; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: color 0.2s, border-color 0.2s; }
    .ldr-tab-btn:hover { color: var(--text-primary); }
    .ldr-tab-btn.ldr-tab-active { color: var(--accent-primary); border-bottom-color: var(--accent-primary); }

    /* Your Research section */
    .ldr-your-research-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.25rem; }
    .ldr-your-research-table { font-size: 0.875rem; width: 100%; border-collapse: collapse; }
    .ldr-your-research-table th { background: var(--bg-tertiary); padding: 0.6rem 0.5rem; text-align: left; border-bottom: 2px solid var(--border-color); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.7rem; }
    .ldr-your-research-table td { padding: 0.55rem 0.5rem; border-bottom: 1px solid var(--border-color); vertical-align: middle; }
    .ldr-your-research-table tr:hover { background: var(--bg-tertiary); }
    .ldr-your-research-empty { text-align: center; padding: 2rem; color: var(--text-secondary); font-size: 0.9rem; }
    .ldr-your-research-chart-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.25rem; }
    .ldr-your-research-chart-container { position: relative; height: 200px; }
    @media (max-width: 768px) {
        .ldr-your-research-stats { grid-template-columns: repeat(2, 1fr); }
        .ldr-your-research-chart-row { grid-template-columns: 1fr; }
    }
</style>
{% endblock %}

{% block content %}
<div class="ldr-journal-quality-container">

    <!-- HEADER -->
    <div class="ldr-metrics-header">
        <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
            <h1 style="margin: 0;">📔 Journal Quality</h1>
            <a href="/metrics/" style="background: var(--bg-tertiary); color: var(--text-primary); padding: 0.5rem 1rem; border-radius: 0.375rem; text-decoration: none; font-size: 0.875rem; border: 1px solid var(--border-color);">← Back to Metrics</a>
        </div>
        <p style="color: var(--text-secondary); margin: 0;">Quality scores for academic journals encountered in your research</p>
    </div>

    <!-- HELP PANEL -->
    {% call help_panel('journal-quality', 'Understanding Journal Quality Scores', icon='graduation-cap', collapsed=true, dismissible=true) %}
    <div class="ldr-help-steps">
        {{ help_step(1, "Automatic Scoring", "When your research encounters a journal for the first time, it is automatically scored using real bibliometric data. Scores are cached so subsequent lookups are instant.") }}
        {{ help_step(2, "Tiered Data Sources", "Scoring uses a tiered approach: (1) Predatory blacklist check, (2) OpenAlex snapshot with h-index data for ~217K journals, (3) DOAJ open access status, (4) LLM analysis as a last resort for unknown journals.") }}
        {{ help_step(3, "Quality Scale (1-10)", "9-10 = Elite (Nature, Science-level), 7-8 = Strong Q1, 5-6 = Moderate, 3-4 = Low, 1-2 = Predatory. Predatory journals are auto-removed from results.") }}
        {{ help_step(4, "Trust Signals", "Badges show: DOAJ listing (open access verified), Scopus indexing, and predatory flags.") }}
        {{ help_step(5, "Data Storage & Refresh (admin)", "The reference database and raw source files are stored at ~/.local/share/local-deep-research/journal_data/ on Linux/macOS, or %APPDATA%\\local-deep-research\\journal_data\\ on Windows. The data is shared across all users on this server, so a forced refresh affects everyone. To update to a newer OpenAlex snapshot: stop the server, delete the files in that directory, and restart — the next search or dashboard visit will re-download in the background. Typically an admin task; normal users don't need to refresh.") }}
    </div>
    {{ help_tip("<strong>Note:</strong> H-index has field-dependent bias — mathematics journals naturally have lower h-index than biomedical journals.") }}
    {% endcall %}

    <!-- ACADEMIC DATA SOURCES BANNER (always visible, near the top) -->
    <div id="ldr-sources-banner" style="margin-bottom: 1rem; padding: 1rem 1.25rem; border-radius: 0.75rem; border: 1px solid var(--border-color); background: var(--bg-secondary);">
        <div style="display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin-bottom: 0.5rem;">
            <div style="font-weight: 600; color: var(--text-primary); font-size: 0.95rem;">
                <i class="fas fa-heart" style="margin-right: 0.4rem; color: var(--accent-primary);"></i>
                Thanks to the open-data projects powering this dashboard
            </div>
            <div style="font-size: 0.75rem; color: var(--text-secondary);">All datasets are downloaded fresh from upstream — nothing is bundled.</div>
        </div>
        <div style="font-size: 0.78rem; color: var(--text-secondary); line-height: 1.45; margin-bottom: 0.75rem;">
            Journal quality scoring is built on freely-licensed bibliographic data from
            <a href="https://openalex.org" target="_blank" rel="noopener noreferrer" style="color: var(--accent-primary); text-decoration: none;">OpenAlex</a> (CC0 — sources &amp; institutions),
            <a href="https://doaj.org" target="_blank" rel="noopener noreferrer" style="color: var(--accent-primary); text-decoration: none;">DOAJ</a> (CC0 — open-access verification),
            <a href="https://github.com/JabRef/abbrv.jabref.org" target="_blank" rel="noopener noreferrer" style="color: var(--accent-primary); text-decoration: none;">JabRef abbreviations</a> (CC0), and
            <a href="https://github.com/stop-predatory-journals/stop-predatory-journals.github.io" target="_blank" rel="noopener noreferrer" style="color: var(--accent-primary); text-decoration: none;">Stop Predatory Journals</a> (MIT).
            Every dataset is fetched directly from the upstream provider on demand — please consider supporting them if you find this useful.
        </div>
        <div id="ldr-sources-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 0.6rem;">
            <!-- Populated by checkDataStatus() — entries injected here -->
        </div>
    </div>

    <!-- THRESHOLD SLIDER (compact, always visible) -->
    <div id="ldr-threshold-banner" style="margin-bottom: 1rem; padding: 1rem 1.25rem; border-radius: 0.75rem; border: 1px solid var(--border-color); background: var(--bg-secondary);">
        <div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 0.5rem;">
            <div style="font-weight: 600; color: var(--text-primary); font-size: 0.95rem;">
                <i class="fas fa-sliders-h" style="margin-right: 0.4rem; color: var(--accent-primary);"></i>
                Journal Quality Filter
                <span style="font-weight: 400; color: var(--text-secondary); font-size: 0.8rem; margin-left: 0.5rem;">
                    threshold = <span id="ldr-threshold-value-top" style="color: var(--accent-primary); font-weight: 600;">…</span>
                </span>
            </div>
            <span id="ldr-threshold-save-status-top" style="font-size: 0.75rem; color: var(--text-secondary);"></span>
        </div>
        <input type="range" id="ldr-threshold-slider-top" min="1" max="10" step="1" value="2"
               style="width: 100%; cursor: pointer;"
               oninput="onThresholdInput(this.value, 'top')"
               onchange="onThresholdChange(this.value, 'top')">
        <div style="display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--text-secondary); margin-top: 0.25rem;">
            <span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span><span>9</span><span>10</span>
        </div>
        <div id="ldr-threshold-explanation-top" style="margin-top: 0.6rem; font-size: 0.8rem; color: var(--text-secondary); line-height: 1.45;">
            <!-- Filled by updateThresholdExplanation() -->
        </div>
        <div style="font-size: 0.72rem; color: var(--text-secondary); margin-top: 0.4rem;">
            See the <a href="#" onclick="switchTab('how-it-works'); return false;" style="color: var(--accent-primary); text-decoration: none;">How It Works</a> tab for the full per-level breakdown.
        </div>
    </div>

    <!-- LOADING -->
    <div id="ldr-loading" class="ldr-loading">
        <div style="font-size: 1.5rem; margin-bottom: 0.5rem;">📊</div>
        <div>Loading journal data...</div>
    </div>

    <!-- ERROR -->
    <div id="ldr-error" class="ldr-error" style="display: none;"></div>

    <!-- DATA STATUS BANNER -->
    <div id="ldr-data-banner" style="display: none; margin-bottom: 1rem; padding: 1rem 1.5rem; border-radius: 0.75rem; border: 1px solid var(--border-color); background: var(--bg-secondary);">
        <div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem;">
            <div id="ldr-data-banner-text" style="color: var(--text-secondary); font-size: 0.9rem;"></div>
            <button id="ldr-data-download-btn" onclick="downloadJournalData()" style="padding: 0.5rem 1rem; border-radius: 0.375rem; border: 1px solid var(--accent-primary); background: var(--accent-primary); color: white; cursor: pointer; font-size: 0.85rem; white-space: nowrap;"></button>
        </div>
        <div id="ldr-data-progress" style="display: none; margin-top: 0.75rem;">
            <div id="ldr-data-progress-intro" style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.5rem;">Downloading...</div>
            <!-- Per-source rows rendered by _renderProgress() -->
            <div id="ldr-data-progress-rows" style="display: flex; flex-direction: column; gap: 0.4rem;"></div>
        </div>
    </div>

    <!-- EMPTY STATE -->
    <div id="ldr-empty" class="ldr-empty-state" style="display: none;">
        <div style="font-size: 4rem; margin-bottom: 1rem; opacity: 0.5;">🔬</div>
        <h2 style="color: var(--text-primary); margin-bottom: 1rem;">No Journal Data Yet</h2>
        <p style="color: var(--text-secondary); max-width: 500px; margin: 0 auto 1.5rem;">
            Journal quality scores are automatically collected as you run research.
            Start a research session with academic sources (ArXiv, OpenAlex, Semantic Scholar, NASA ADS) enabled.
        </p>
        <a href="/" style="display: inline-block; background: var(--accent-primary); color: white; padding: 0.75rem 1.5rem; border-radius: 0.375rem; text-decoration: none;">
            <i class="fas fa-search"></i> Start Your First Research
        </a>
    </div>

    <!-- MAIN CONTENT -->
    <div id="ldr-content" style="display: none;">

        <!-- TAB NAVIGATION
             Defaults to "Your Research" — when a user opens this page they
             usually want to see how the system has scored the journals they
             actually encountered in their searches, not a list of every
             journal in the bundled reference DB. -->
        <div class="ldr-tab-bar">
            <button class="ldr-tab-btn ldr-tab-active" data-tab="your-research" onclick="switchTab('your-research')">Your Research</button>
            <button class="ldr-tab-btn" data-tab="global-db" onclick="switchTab('global-db')">Global Database</button>
            <button class="ldr-tab-btn" data-tab="how-it-works" onclick="switchTab('how-it-works')">How It Works</button>
        </div>

        <!-- TAB: HOW IT WORKS -->
        <div id="ldr-tab-how-it-works" class="ldr-tab-panel" style="display:none;">
            <div class="ldr-metric-card" style="margin-bottom: 1rem;">
                <h3 style="margin-top:0;">What this dashboard does</h3>
                <p style="color: var(--text-secondary); line-height: 1.55;">
                    Every time your research touches an academic paper, LDR resolves the source venue (journal or conference)
                    and assigns it a quality score from <strong>1 to 10</strong>. The score is cached in your local DB so
                    the same journal is never scored twice. The <em>Journal Reputation Filter</em> uses these scores to
                    automatically remove predatory venues from your results and to surface high-quality sources first.
                </p>
            </div>

            <div class="ldr-metric-card" style="margin-bottom: 1rem;">
                <h3 style="margin-top:0;">Tiered scoring pipeline</h3>
                <p style="color: var(--text-secondary); margin-top:0;">
                    Each journal flows through these tiers in order. The first tier that returns a score wins, so the
                    expensive tiers (institution lookup, LLM) only run when the cheap ones can't answer.
                </p>
                <ol style="color: var(--text-secondary); line-height: 1.7; padding-left: 1.25rem;">
                    <li><strong>Tier 0 — Local cache.</strong> Already scored in your per-user DB? Return instantly.</li>
                    <li><strong>Tier 1 — Predatory blacklist.</strong> Match against
                        <a href="https://github.com/stop-predatory-journals/stop-predatory-journals.github.io" target="_blank" rel="noopener noreferrer" style="color: var(--accent-primary);">Stop Predatory Journals</a>
                        (~2.5K entries). A hit forces score 1 and the result is dropped from your search.</li>
                    <li><strong>Tier 2 — OpenAlex sources.</strong> ~280K journals + conferences with h-index, 2-yr mean
                        citedness, citation count, publisher, ISSN. Quality is derived from h-index thresholds
                        (≥150 → 10, ≥75 → 8, ≥40 → 7, ≥20 → 6, ≥10 → 5, else → 4) and a Q1–Q4 quartile is computed
                        from cited_by_count percentiles per source type.</li>
                    <li><strong>Tier 3 — DOAJ.</strong> ~22K verified open-access journals. DOAJ listing → 5.
                        Used when OpenAlex doesn't have a row.</li>
                    <li><strong>Tier 3.5 — Institution affiliation salvage.</strong> For preprints (arXiv, bioRxiv, …)
                        without a journal, fall back to scoring the author's institution using ~120K OpenAlex Institutions
                        records (h-index ≥ 250 → 6, ≥ 50 → 5, else → 4). Capped — never beats a real venue match.</li>
                    <li><strong>Tier 4 — LLM analysis (optional, off by default).</strong> A last-resort SearXNG + LLM lookup
                        for journals not in any of the bundled datasets. Enable it under
                        <code>Settings → Search → Journal Reputation → Enable LLM Scoring</code>.
                        Results are persisted to the cache so the slow path runs at most once per journal.</li>
                </ol>
            </div>

            <div class="ldr-metric-card" style="margin-bottom: 1rem;">
                <h3 style="margin-top:0;">Quality scale (1–10)</h3>
                <ul style="color: var(--text-secondary); line-height: 1.65; padding-left: 1.25rem; list-style: none;">
                    <li><span style="display:inline-block; width:3.5rem; font-weight:600; color: var(--success-color);">9–10</span> Elite (Nature, Science, NEJM, top conferences)</li>
                    <li><span style="display:inline-block; width:3.5rem; font-weight:600; color: var(--success-color);">7–8</span> Strong Q1 (well-established field-leading venues)</li>
                    <li><span style="display:inline-block; width:3.5rem; font-weight:600; color: var(--warning-color);">5–6</span> Moderate (legitimate, peer-reviewed)</li>
                    <li><span style="display:inline-block; width:3.5rem; font-weight:600; color: var(--warning-color);">3–4</span> Low / unknown</li>
                    <li><span style="display:inline-block; width:3.5rem; font-weight:600; color: var(--error-color);">1–2</span> Predatory — automatically filtered out</li>
                </ul>
                <p style="color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 0;">
                    <strong>Caveat:</strong> h-index has field-dependent bias — mathematics journals naturally have a lower
                    h-index than biomedical journals. Quartile (Q1–Q4) is normalized within source type to partially compensate.
                </p>
            </div>

            <div class="ldr-metric-card" style="margin-bottom: 1rem;">
                <h3 style="margin-top:0;">Trust signals</h3>
                <ul style="color: var(--text-secondary); line-height: 1.65;">
                    <li><strong>DOAJ</strong> — listed as a verified open-access journal</li>
                    <li><strong>Scopus / Core</strong> — indexed in OpenAlex's <em>core</em> set</li>
                    <li><strong>Predatory</strong> — appears on the Stop Predatory Journals blacklist <em>and</em> is not rescued by the whitelist (DOAJ-listed or h-index above the `PREDATORY_WHITELIST_HINDEX` threshold)</li>
                </ul>
            </div>

            <div class="ldr-metric-card" style="margin-bottom: 1rem;">
                <h3 style="margin-top:0;">Threshold setting (how aggressive is the filter?)</h3>
                <p style="color: var(--text-secondary); margin-top:0;">
                    Drag the slider to change <code>search.journal_reputation.threshold</code> directly.
                    Changes save automatically and apply to the next search.
                </p>

                <div style="margin: 1.25rem 0; padding: 1rem 1.25rem; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 0.5rem;">
                    <div style="display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 0.5rem;">
                        <label for="ldr-threshold-slider" style="font-weight: 600; color: var(--text-primary);">
                            Quality Threshold: <span id="ldr-threshold-value" style="color: var(--accent-primary); font-size: 1.1rem;">…</span>
                        </label>
                        <span id="ldr-threshold-save-status" style="font-size: 0.75rem; color: var(--text-secondary);"></span>
                    </div>
                    <input type="range" id="ldr-threshold-slider" min="1" max="10" step="1" value="2"
                           style="width: 100%; cursor: pointer;"
                           oninput="onThresholdInput(this.value)"
                           onchange="onThresholdChange(this.value)">
                    <div style="display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--text-secondary); margin-top: 0.25rem;">
                        <span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span><span>9</span><span>10</span>
                    </div>
                    <div id="ldr-threshold-explanation" style="margin-top: 0.85rem; padding: 0.75rem; background: var(--bg-secondary); border-radius: 0.375rem; font-size: 0.85rem; color: var(--text-secondary); line-height: 1.5;">
                        <!-- Filled by updateThresholdExplanation() -->
                    </div>
                </div>

                <ul style="color: var(--text-secondary); line-height: 1.65; font-size: 0.85rem;">
                    <li><strong>1</strong> — Filter is effectively off. Nothing is removed (predatory journals score 1, but threshold 1 means "≥ 1" so they pass).</li>
                    <li><strong>2 (default)</strong> — Drop only predatory (score 1). Keep everything else, including default/unknown venues.</li>
                    <li><strong>3</strong> — Same as 2 (no scores fall in this gap — values 2, 3, 9 are never emitted).</li>
                    <li><strong>4</strong> — Same as 2 (no scores fall in this gap).</li>
                    <li><strong>5</strong> — Also drop default/unknown venues (score 4). Repository aggregators, garbled refs, brand-new or niche venues with no h-index data.</li>
                    <li><strong>6</strong> — Also drop moderate journals (score 5). Conferences and DOAJ-only OA journals start to disappear.</li>
                    <li><strong>7</strong> — Keep only good journals (h-index ≥ 20).</li>
                    <li><strong>8</strong> — Keep only strong Q1 journals (h-index ≥ 40).</li>
                    <li><strong>9–10</strong> — Elite only. Nature, Science, NEJM tier (h-index ≥ 75 / ≥ 150). Aggressive — most search results will be filtered out.</li>
                </ul>
                <p style="color: var(--text-secondary); font-size: 0.8rem; margin-bottom: 0; font-style: italic;">
                    The default of 2 is intentionally conservative: it removes flagged predatory venues
                    (we have positive evidence of fraud) but doesn't silently delete sources just because
                    we don't have bibliometric data on them.
                </p>
            </div>

            <div class="ldr-metric-card">
                <h3 style="margin-top:0;">Using the filter in research</h3>
                <p style="color: var(--text-secondary); line-height: 1.55; margin-top: 0;">
                    The <em>Journal Reputation Filter</em> is enabled per-search-engine in
                    <code>Settings → Search → Journal Reputation</code>. It's designed for
                    academic engines (ArXiv, OpenAlex, Semantic Scholar, NASA ADS, PubMed). When active it:
                </p>
                <ul style="color: var(--text-secondary); line-height: 1.65;">
                    <li>Scores every result through the tiered pipeline above</li>
                    <li>Drops results below the threshold (default 2 = predatory only)</li>
                    <li>Records the score on the result for downstream ranking and citation export</li>
                    <li>Triggers a one-time dataset download on first use (~7 min, ~25 MB) — afterwards everything is local</li>
                </ul>
                <p style="color: var(--text-secondary); font-size: 0.85rem; margin-top: 1rem; margin-bottom: 0;">
                    Full documentation:
                    <a href="https://github.com/LearningCircuit/local-deep-research/blob/main/docs/journal-quality.md"
                       target="_blank" rel="noopener noreferrer" style="color: var(--accent-primary);">
                        docs/journal-quality.md on GitHub
                    </a>
                </p>
            </div>
        </div>

        <!-- TAB: YOUR RESEARCH -->
        <div id="ldr-tab-your-research" class="ldr-tab-panel" style="display:none;">
            <div id="ldr-yr-loading" style="text-align:center; padding:2.5rem; color:var(--text-secondary);">
                <div style="font-size: 1.5rem; margin-bottom: 0.5rem;">Loading your research data...</div>
            </div>
            <div id="ldr-yr-empty" style="display:none; text-align:center; padding:3rem;">
                <div style="font-size:3rem; margin-bottom:1rem; opacity:0.4;">📔</div>
                <h3 style="color:var(--text-primary); margin-bottom:0.5rem;">No Journals Encountered Yet</h3>
                <p style="color:var(--text-secondary); max-width:450px; margin:0 auto 1.5rem;">
                    Journal quality data appears here as you run research with academic search engines (ArXiv, OpenAlex, Semantic Scholar, NASA ADS).
                </p>
                <a href="/" style="display:inline-block; background:var(--accent-primary); color:white; padding:0.6rem 1.25rem; border-radius:0.375rem; text-decoration:none; font-size:0.9rem;">Start a Research</a>
            </div>
            <div id="ldr-yr-content" style="display:none;">
                <div class="ldr-your-research-stats">
                    <div class="ldr-stat-item">
                        <span class="ldr-stat-value" id="yr-stat-journals">0</span>
                        <div class="ldr-stat-label">Journals Seen</div>
                    </div>
                    <div class="ldr-stat-item">
                        <span class="ldr-stat-value" id="yr-stat-avg-quality">—</span>
                        <div class="ldr-stat-label">Avg Quality</div>
                    </div>
                    <div class="ldr-stat-item">
                        <span class="ldr-stat-value" id="yr-stat-papers">0</span>
                        <div class="ldr-stat-label">Papers Linked</div>
                    </div>
                    <div class="ldr-stat-item">
                        <span class="ldr-stat-value" id="yr-stat-predatory" style="color:var(--error-color);">0</span>
                        <div class="ldr-stat-label">Predatory Blocked</div>
                    </div>
                </div>
                <div class="ldr-your-research-chart-row">
                    <div>
                        <h4 style="margin:0 0 0.5rem 0; font-size:0.85rem; color:var(--text-secondary);">Quality Distribution</h4>
                        <div class="ldr-your-research-chart-container"><canvas id="ldr-yr-quality-chart"></canvas></div>
                    </div>
                    <div>
                        <h4 style="margin:0 0 0.5rem 0; font-size:0.85rem; color:var(--text-secondary);">Score Sources</h4>
                        <div class="ldr-your-research-chart-container"><canvas id="ldr-yr-source-chart"></canvas></div>
                    </div>
                </div>
                <div class="ldr-metric-card" style="margin-top:1rem;">
                    <h3>Journals in Your Research (<span id="yr-table-count">0</span>)</h3>
                    <div style="overflow-x:auto;">
                        <table class="ldr-your-research-table">
                            <thead>
                                <tr>
                                    <th scope="col" style="width:2rem;">#</th>
                                    <th scope="col">Journal</th>
                                    <th scope="col">Quality</th>
                                    <th scope="col">Quartile</th>
                                    <th scope="col" style="text-align:right;">Papers</th>
                                    <th scope="col" style="text-align:center;">Years</th>
                                    <th scope="col">Source</th>
                                    <th scope="col">Trust</th>
                                </tr>
                            </thead>
                            <tbody id="ldr-yr-tbody"></tbody>
                        </table>
                    </div>
                </div>
            </div>
        </div>

        <!-- TAB: GLOBAL DATABASE -->
        <div id="ldr-tab-global-db" class="ldr-tab-panel">

            <!-- SUMMARY STATS -->
            <div class="ldr-metric-card" style="margin-bottom: 1.5rem;">
                <h3>Overview</h3>
                <div class="ldr-overall-stats">
                    <div class="ldr-stat-item">
                        <span class="ldr-stat-value" id="stat-total">0</span>
                        <div class="ldr-stat-label">Total Journals</div>
                    </div>
                    <div class="ldr-stat-item">
                        <span class="ldr-stat-value" id="stat-avg-quality">0</span>
                        <div class="ldr-stat-label">Avg Quality</div>
                    </div>
                    <div class="ldr-stat-item">
                        <span class="ldr-stat-value" id="stat-predatory" style="color: var(--error-color);">0</span>
                        <div class="ldr-stat-label">Predatory</div>
                    </div>
                    <div class="ldr-stat-item">
                        <span class="ldr-stat-value" id="stat-doaj" style="color: var(--success-color);">0</span>
                        <div class="ldr-stat-label">DOAJ Listed</div>
                    </div>
                    <div class="ldr-stat-item">
                        <span class="ldr-stat-value" id="stat-h-index">0</span>
                        <div class="ldr-stat-label">Avg H-Index</div>
                    </div>
                </div>
            </div>

            <!-- CHARTS -->
            <div class="ldr-metrics-grid">
                <div class="ldr-metric-card">
                    <h3>Quality Distribution</h3>
                    <div class="ldr-chart-container"><canvas id="ldr-quality-dist-chart"></canvas></div>
                </div>
                <div class="ldr-metric-card">
                    <h3>Score Sources</h3>
                    <div class="ldr-chart-container"><canvas id="ldr-score-source-chart"></canvas></div>
                </div>
            </div>

            <!-- JOURNAL TABLE -->
        <div class="ldr-metric-card">
            <h3>📋 Journal Database (<span id="ldr-table-count">0</span> journals)</h3>
            <div class="ldr-journal-controls">
                <label for="ldr-journal-search" class="sr-only">Search journals by name</label>
                <input type="text" id="ldr-journal-search" placeholder="Search journals by name...">
                <label for="ldr-filter-tier" class="sr-only">Filter by quality tier</label>
                <select id="ldr-filter-tier">
                    <option value="">All Tiers</option>
                    <option value="elite">Elite (9-10)</option>
                    <option value="strong">Strong (7-8)</option>
                    <option value="moderate">Moderate (5-6)</option>
                    <option value="low">Low (3-4)</option>
                    <option value="predatory">Predatory (1-2)</option>
                </select>
                <label for="ldr-filter-source" class="sr-only">Filter by score source</label>
                <select id="ldr-filter-source">
                    <option value="">All Sources</option>
                    <option value="openalex">OpenAlex</option>
                    <option value="doaj">DOAJ</option>
                    <option value="llm">LLM</option>
                </select>
            </div>
            <div class="ldr-journal-table-wrapper">
                <table class="ldr-journal-table">
                    <thead>
                        <tr>
                            <th scope="col" data-sort="name">Name</th>
                            <th scope="col" data-sort="quality">Quality</th>
                            <th scope="col" data-sort="quartile">Quartile</th>
                            <th scope="col" data-sort="h_index" class="ldr-col-right">H-Index</th>
                            <th scope="col" data-sort="impact_factor" class="ldr-col-right ldr-col-impact">IF</th>
                            <th scope="col" data-sort="score_source" class="ldr-col-source">Source</th>
                            <th scope="col">Trust</th>
                            <th scope="col" data-sort="publisher" class="ldr-col-publisher">Publisher</th>
                        </tr>
                    </thead>
                    <tbody id="ldr-journal-tbody"></tbody>
                </table>
            </div>
            <div class="ldr-pagination" id="ldr-pagination"></div>
            <div class="ldr-table-info" id="ldr-table-info"></div>
        </div>

        </div><!-- /ldr-tab-global-db -->
    </div><!-- /ldr-content -->

    <!-- Footer attribution removed: replaced by the always-visible
         #ldr-sources-banner near the top of the page. -->
</div>

<script>
/* ------------------------------------------------------------------ */
/* State — server-side pagination, no client-side data array           */
/* ------------------------------------------------------------------ */
let qualityDistChart = null;
let scoreSourceChart = null;
let currentPage = 1;
let totalPages = 1;
let totalCount = 0;
const PAGE_SIZE = 50;
let sortField = 'quality';
let sortDir = 'desc';
let yrQualityChart = null;
let yrSourceChart = null;
let globalDbLoaded = false;
let userResearchLoaded = false;

/* ------------------------------------------------------------------ */
/* Tab switching                                                       */
/* ------------------------------------------------------------------ */
function switchTab(tab) {
    document.querySelectorAll('.ldr-tab-btn').forEach(btn => {
        btn.classList.toggle('ldr-tab-active', btn.dataset.tab === tab);
    });
    document.querySelectorAll('.ldr-tab-panel').forEach(panel => {
        panel.style.display = 'none';
    });
    document.getElementById('ldr-tab-' + tab).style.display = 'block';

    // Lazy-load global DB on first switch
    if (tab === 'global-db' && !globalDbLoaded) {
        globalDbLoaded = true;
        loadJournalPage(1, true);
    }
    // Lazy-load user research on first switch
    if (tab === 'your-research' && !userResearchLoaded) {
        userResearchLoaded = true;
        loadUserResearchJournals();
    }
    // Lazy-load current threshold value when help tab opens
    if (tab === 'how-it-works' && !thresholdLoaded) {
        thresholdLoaded = true;
        loadThreshold();
    }
}

/* ------------------------------------------------------------------ */
/* Threshold slider                                                    */
/* ------------------------------------------------------------------ */
let thresholdLoaded = false;
let thresholdSaveTimer = null;

const THRESHOLD_DESCRIPTIONS = {
    1: { label: 'Filter is OFF', detail: 'Nothing is removed. Even predatory journals (score 1) pass because the filter keeps everything ≥ 1.' },
    2: { label: 'Predatory only (default)', detail: 'Drops only confirmed predatory journals from the Stop Predatory Journals list — minus any rescued by the whitelist (DOAJ-listed or h-index above PREDATORY_WHITELIST_HINDEX). Default/unknown and low-h-index journals are kept.' },
    3: { label: 'Predatory only', detail: 'Identical to threshold 2 — no journal scores fall in this gap (values 2, 3, 9 are never emitted).' },
    4: { label: 'Predatory only', detail: 'Identical to threshold 2 — no journal scores fall in this gap.' },
    5: { label: 'Predatory + unknowns', detail: 'Also drops journals with no signal in any bundled dataset (score 4): repository aggregators, garbled refs, brand-new venues with no h-index data.' },
    6: { label: 'Moderate quality and up', detail: 'Also drops moderate journals (score 5). Conferences and DOAJ-only OA journals start to disappear.' },
    7: { label: 'Good quality and up', detail: 'Keeps only journals with h-index > 20. About 60% of OpenAlex sources fall below this.' },
    8: { label: 'Strong Q1 only', detail: 'Keeps only strong Q1 journals (h-index > 40). Most search results will be filtered.' },
    9: { label: 'Elite Q1 only', detail: 'Keeps only top journals with h-index > 75. Very aggressive — expect ~80–90% of results to be removed.' },
    10: { label: 'Nature/Science tier only', detail: 'Keeps only Nature/Science/NEJM-tier journals (h-index > 150). Almost everything will be filtered out.' },
};

function updateThresholdExplanation(value) {
    const v = parseInt(value, 10);
    const info = THRESHOLD_DESCRIPTIONS[v] || { label: '', detail: '' };
    // Help-tab widget (full)
    const valEl = document.getElementById('ldr-threshold-value');
    if (valEl) valEl.textContent = v;
    const expEl = document.getElementById('ldr-threshold-explanation');
    if (expEl) expEl.innerHTML = '<strong>' + info.label + '</strong><br>' + info.detail;
    // Top-of-page widget (compact)
    const valTop = document.getElementById('ldr-threshold-value-top');
    if (valTop) valTop.textContent = v;
    const expTop = document.getElementById('ldr-threshold-explanation-top');
    if (expTop) expTop.innerHTML = '<strong>' + info.label + '</strong> — ' + info.detail;
    // Sync the other slider so they always show the same value
    ['ldr-threshold-slider', 'ldr-threshold-slider-top'].forEach(id => {
        const s = document.getElementById(id);
        if (s && s.value != v) s.value = v;
    });
}

function onThresholdInput(value, _src) {
    // Live preview while dragging — no save yet
    updateThresholdExplanation(value);
}

function onThresholdChange(value, _src) {
    // Debounce save by 300 ms in case the user keeps moving
    updateThresholdExplanation(value);
    clearTimeout(thresholdSaveTimer);
    ['ldr-threshold-save-status', 'ldr-threshold-save-status-top'].forEach(id => {
        const s = document.getElementById(id);
        if (s) { s.textContent = 'Saving…'; s.style.color = 'var(--text-secondary)'; }
    });
    thresholdSaveTimer = setTimeout(() => saveThreshold(parseInt(value, 10)), 300);
}

async function loadThreshold() {
    try {
        const resp = await fetch('/settings/api/search.journal_reputation.threshold');
        if (!resp.ok) throw new Error('HTTP ' + resp.status);
        const data = await resp.json();
        const value = (data.setting && data.setting.value) ?? data.value ?? 2;
        updateThresholdExplanation(value);
    } catch (e) {
        console.error('Failed to load threshold setting:', e);
        updateThresholdExplanation(2);
    }
}

async function saveThreshold(value) {
    const setStatus = (text, color) => {
        ['ldr-threshold-save-status', 'ldr-threshold-save-status-top'].forEach(id => {
            const s = document.getElementById(id);
            if (s) { s.textContent = text; s.style.color = color; }
        });
    };
    try {
        const csrf = document.querySelector('meta[name="csrf-token"]');
        const headers = { 'Content-Type': 'application/json' };
        if (csrf) headers['X-CSRFToken'] = csrf.content;
        const resp = await fetch('/settings/api/search.journal_reputation.threshold', {
            method: 'PUT',
            headers: headers,
            body: JSON.stringify({ value: value })
        });
        if (!resp.ok) throw new Error('HTTP ' + resp.status);
        setStatus('✓ Saved', 'var(--success-color, #34c759)');
        setTimeout(() => setStatus('', 'var(--text-secondary)'), 2000);
        // Global toast — same green slide-in the rest of the app uses
        // when a setting is updated.
        if (window.ui && window.ui.showMessage) {
            window.ui.showMessage(
                'Journal quality threshold updated to ' + value,
                'success'
            );
        }
    } catch (e) {
        console.error('Failed to save threshold:', e);
        setStatus('✗ Save failed', 'var(--error-color, #ff6347)');
        if (window.ui && window.ui.showMessage) {
            window.ui.showMessage('Failed to save threshold setting', 'error');
        }
    }
}

/* ------------------------------------------------------------------ */
/* Journal data download management                                    */
/* ------------------------------------------------------------------ */
/* Every s.* field interpolated below MUST go through escHtml (text
 * nodes) or safeHref (href values). DataSource attribute values are
 * Python string literals today (defense-in-depth), but any future
 * DataSource subclass that reads from network or DB must not be able
 * to inject HTML. Do not add new s.* interpolations without escaping. */
function renderSourcesBanner(sources) {
    const grid = document.getElementById('ldr-sources-grid');
    if (!grid || !Array.isArray(sources)) return;
    grid.innerHTML = sources.map(s => {
        const ok = !!s.present;
        const pillBg = ok ? 'rgba(52,199,89,0.15)' : 'rgba(255,99,71,0.15)';
        const pillColor = ok ? '#34c759' : '#ff6347';
        const pillIcon = ok ? '✓' : '✗';
        const pillLabel = ok ? 'Downloaded' : 'Not downloaded';
        const datasetLink = s.dataset_url
            ? `<a href="${safeHref(s.dataset_url)}" target="_blank" rel="noopener noreferrer" style="color: var(--accent-primary); text-decoration: none;" title="Direct link to the upstream dataset/dump"><i class="fas fa-database" style="margin-right: 0.2rem;"></i>Dataset</a>`
            : '';
        return `
            <div style="padding: 0.6rem 0.75rem; background: var(--bg-primary); border: 1px solid var(--border-color); border-radius: 0.5rem;">
                <div style="display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-bottom: 0.3rem;">
                    <a href="${safeHref(s.url)}" target="_blank" rel="noopener noreferrer" style="font-weight: 600; color: var(--text-primary); text-decoration: none; font-size: 0.85rem;">${escHtml(s.name)}</a>
                    <span style="display: inline-flex; align-items: center; gap: 0.2rem; padding: 0.15rem 0.45rem; border-radius: 999px; background: ${pillBg}; color: ${pillColor}; font-size: 0.7rem; font-weight: 600; white-space: nowrap;">${pillIcon} ${pillLabel}</span>
                </div>
                <div style="font-size: 0.72rem; color: var(--text-secondary); line-height: 1.35; margin-bottom: 0.3rem;">${escHtml(s.description || '')}</div>
                <div style="display: flex; gap: 0.75rem; font-size: 0.7rem;">
                    <a href="${safeHref(s.license_url)}" target="_blank" rel="noopener noreferrer" style="color: var(--accent-primary); text-decoration: none;">License: ${escHtml(s.license)}</a>
                    ${datasetLink}
                </div>
            </div>
        `;
    }).join('');
}

async function checkDataStatus() {
    try {
        const resp = await fetch('/metrics/api/journal-data/status');
        const status = await resp.json();
        const banner = document.getElementById('ldr-data-banner');
        const text = document.getElementById('ldr-data-banner-text');
        const btn = document.getElementById('ldr-data-download-btn');

        // Always render the per-source grid, regardless of overall available state.
        renderSourcesBanner(status.sources || []);

        if (!status.available) {
            banner.style.display = 'block';
            text.textContent = 'Journal quality data is not installed. Fetch it from OpenAlex (sources + institutions), DOAJ, JabRef, and Stop Predatory Journals to enable scoring and this dashboard (~7 min one-time download, ~25 MB on disk).';
            btn.textContent = 'Download Data';
        } else if (status.needs_update) {
            banner.style.display = 'block';
            text.textContent = 'A newer version of journal data is available (' + status.latest_version + ').';
            btn.textContent = 'Update Data';
        } else {
            banner.style.display = 'none';
        }
        return status.available;
    } catch (e) {
        return true; // Assume available on error
    }
}

let _ldrProgressTimer = null;

function _stateColor(state) {
    // Keep in sync with the dashboard's palette vars defined in app CSS.
    if (state === 'success') return 'var(--success-color, #2ecc71)';
    if (state === 'error')   return 'var(--error-color, #fa5c7c)';
    if (state === 'running') return 'var(--accent-primary, #0b84ff)';
    return 'var(--bg-tertiary, #444)'; // pending
}

function _stateIcon(state) {
    if (state === 'success') return '✓';
    if (state === 'error')   return '✗';
    if (state === 'running') return '⟳';
    return '·';
}

function _renderProgress(snap) {
    const progress = document.getElementById('ldr-data-progress');
    const intro    = document.getElementById('ldr-data-progress-intro');
    const rowsEl   = document.getElementById('ldr-data-progress-rows');
    if (!progress || !rowsEl || !intro) return;
    progress.style.display = 'block';

    const sources = snap.sources || {};
    const dbBuild = snap.db_build || {state: 'pending'};
    const total   = Object.keys(sources).length + 1; // +1 for db_build
    const done    = Object.values(sources).filter(s => s.state === 'success' || s.state === 'error').length
                  + ((dbBuild.state === 'success' || dbBuild.state === 'error') ? 1 : 0);
    intro.textContent = total > 0
        ? `${done}/${total} steps complete` + (snap.state === 'running' ? ' — downloads run in parallel' : '')
        : 'Starting...';

    // Build rows: one per source + one for db build.
    const entries = Object.entries(sources).map(([key, info]) => ({
        key, name: info.name || key,
        state: info.state || 'pending',
        detail: info.detail || '',
        percent: info.percent || 0,
    }));
    entries.push({
        key: '__db_build__',
        name: 'Build reference database',
        state: dbBuild.state || 'pending',
        detail: dbBuild.detail || '',
        percent: dbBuild.state === 'running' ? 50 : 0,
    });

    rowsEl.innerHTML = entries.map(row => {
        // Use the server-reported per-source percent when running; fall
        // back to 0 / 100 at the ends so the bar snaps cleanly.
        let width = 0;
        if (row.state === 'success' || row.state === 'error') width = 100;
        else if (row.state === 'running') {
            width = (typeof row.percent === 'number' && row.percent > 0) ? row.percent : 5;
        }
        return `
        <div style="display: flex; align-items: center; gap: 0.6rem; font-size: 0.8rem;">
            <span style="flex: 0 0 1.1rem; text-align: center; color: ${_stateColor(row.state)};">${_stateIcon(row.state)}</span>
            <span style="flex: 0 0 11rem; color: var(--text-primary);">${escHtml(row.name)}</span>
            <span style="flex: 1 1 auto; background: var(--bg-tertiary); border-radius: 3px; height: 6px; overflow: hidden;">
                <span style="display: block; height: 100%; width: ${width}%; background: ${_stateColor(row.state)}; transition: width 0.3s;"></span>
            </span>
            <span style="flex: 0 0 auto; color: var(--text-secondary); font-size: 0.75rem;">${escHtml(row.detail)}</span>
        </div>`;
    }).join('');
}

async function _pollDownloadProgress() {
    try {
        const resp = await fetch('/metrics/api/journal-data/status', {
            headers: {'Accept': 'application/json'},
        });
        if (!resp.ok) return;
        const status = await resp.json();
        const snap = status.download_progress || {state: 'idle'};
        if (snap.state === 'running') _renderProgress(snap);
    } catch (e) {
        // Polling is best-effort; swallow network hiccups silently.
    }
}

async function downloadJournalData() {
    const btn = document.getElementById('ldr-data-download-btn');

    btn.disabled = true;
    btn.textContent = 'Downloading...';
    // Render an initial "starting" snapshot so the user sees the rows
    // appear immediately — the first /status poll may be ~2s away.
    _renderProgress({state: 'running', sources: {}, db_build: {state: 'pending'}});

    // Poll the status endpoint while the POST is in flight so the user
    // sees real per-source progress instead of a frozen bar.
    if (_ldrProgressTimer) clearInterval(_ldrProgressTimer);
    _ldrProgressTimer = setInterval(_pollDownloadProgress, 2000);

    try {
        const csrfToken = window.api ? window.api.getCsrfToken() : '';
        const resp = await fetch('/metrics/api/journal-data/download', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRFToken': csrfToken
            },
            body: JSON.stringify({force: false})
        });
        const result = await resp.json();

        if (result.success) {
            progressBar.style.width = '100%';
            progressText.textContent = '[100%] Complete — ' + result.message;
            setTimeout(() => location.reload(), 1500);
        } else {
            progressText.textContent = 'Error: ' + result.message;
            progressBar.style.width = '0%';
            btn.disabled = false;
            btn.textContent = 'Retry Download';
        }
    } catch (e) {
        progressText.textContent = 'Network error: ' + e.message;
        btn.disabled = false;
        btn.textContent = 'Retry Download';
    } finally {
        if (_ldrProgressTimer) {
            clearInterval(_ldrProgressTimer);
            _ldrProgressTimer = null;
        }
    }
}

// On dashboard load, if a download is already in progress (e.g. the
// user reloaded the page mid-download, or a background init triggered
// one), show its live status immediately instead of a blank banner.
document.addEventListener('DOMContentLoaded', async () => {
    try {
        const resp = await fetch('/metrics/api/journal-data/status');
        if (!resp.ok) return;
        const status = await resp.json();
        const snap = status.download_progress;
        if (snap && snap.state === 'running') {
            const btn = document.getElementById('ldr-data-download-btn');
            if (btn) {
                btn.disabled = true;
                btn.textContent = 'Downloading...';
            }
            _renderProgress(snap);
            _ldrProgressTimer = setInterval(_pollDownloadProgress, 2000);
        }
    } catch (e) {
        // Best-effort; not critical if this probe fails.
    }
});

// Dark theme: set Chart.js default text color from CSS variable
const textColor = getComputedStyle(document.documentElement)
    .getPropertyValue('--text-secondary').trim() || '#aaa';

/* ------------------------------------------------------------------ */
/* HTML escaping (prevents XSS from journal names/publishers)          */
/* ------------------------------------------------------------------ */
function escHtml(s) {
    return String(s ?? '')
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
}

/* Reject javascript:/data:/unsafe schemes; accept http(s), mailto,
 * rooted paths. .trim() defends against "  javascript:..." bypasses;
 * the ^ anchor already rejects leading-whitespace versions, but
 * .trim() + anchor together encode the invariant explicitly.
 * Do NOT relax this to accept bare "#" — '#' is the failure value
 * only; treating # as an allowed prefix opens fragment-injection
 * vectors. */
function safeHref(s) {
    const v = String(s ?? '').trim();
    return /^(https?:|mailto:|\/)/i.test(v) ? escHtml(v) : '#';
}

/* ------------------------------------------------------------------ */
/* Quality tier helpers                                                */
/* ------------------------------------------------------------------ */
function getQualityTier(score) {
    if (score == null) return { cls: 'ldr-quality-unknown', label: '?' };
    if (score <= 2) return { cls: 'ldr-quality-dangerous', label: score };
    if (score <= 4) return { cls: 'ldr-quality-low', label: score };
    if (score <= 6) return { cls: 'ldr-quality-moderate', label: score };
    if (score <= 8) return { cls: 'ldr-quality-strong', label: score };
    return { cls: 'ldr-quality-elite', label: score };
}

// Returns the HTML for a quartile chip (Q1=green, Q2=blue, Q3=yellow,
// Q4=orange, anything else = "—" muted dash). Tolerates the field
// being absent / null / lowercase.
function getQuartileChip(quartile) {
    if (!quartile) return '<span class="ldr-quartile-unknown">—</span>';
    const q = String(quartile).toUpperCase().trim();
    const knownClasses = { Q1: 'ldr-quartile-q1', Q2: 'ldr-quartile-q2', Q3: 'ldr-quartile-q3', Q4: 'ldr-quartile-q4' };
    const cls = knownClasses[q];
    if (!cls) return '<span class="ldr-quartile-unknown">—</span>';
    return `<span class="ldr-quartile-chip ${cls}">${q}</span>`;
}

function getBarColors() {
    return [
        'rgba(250,92,124,0.8)', 'rgba(250,92,124,0.8)',
        'rgba(249,188,11,0.8)', 'rgba(249,188,11,0.8)',
        'rgba(64,191,255,0.8)', 'rgba(64,191,255,0.8)',
        'rgba(10,207,151,0.8)', 'rgba(10,207,151,0.8)',
        'rgba(124,92,252,0.8)', 'rgba(124,92,252,0.8)',
    ];
}

function getSourceBadge(source) {
    const map = {
        openalex: { cls: 'ldr-source-openalex', label: 'OpenAlex' },
        doaj: { cls: 'ldr-source-doaj', label: 'DOAJ' },
        llm: { cls: 'ldr-source-llm', label: 'LLM' },
        predatory: { cls: 'ldr-source-predatory', label: 'Predatory' },
    };
    const m = map[source] || { cls: '', label: source || 'unknown' };
    return `<span class="ldr-source-badge ${m.cls}">${escHtml(m.label)}</span>`;
}

function getTrustIcons(j) {
    let icons = '';
    if (j.is_in_doaj) icons += '<span class="ldr-trust-icon" title="DOAJ Listed" style="color:var(--success-color);">✓</span>';
    if (j.is_predatory) icons += '<span class="ldr-trust-icon" title="Predatory: ' + escHtml(j.predatory_source||'') + '" style="color:var(--error-color);">⚠</span>';
    return icons || '<span style="color:var(--text-muted);">—</span>';
}

/* ------------------------------------------------------------------ */
/* Build API URL from current filter/sort/page state                   */
/* ------------------------------------------------------------------ */
function buildApiUrl(page, includeSummary) {
    const search = document.getElementById('ldr-journal-search').value || '';
    const tier = document.getElementById('ldr-filter-tier').value || '';
    const source = document.getElementById('ldr-filter-source').value || '';
    const params = new URLSearchParams({
        page: page, per_page: PAGE_SIZE,
        sort: sortField, order: sortDir,
    });
    if (search) params.set('search', search);
    if (tier) params.set('tier', tier);
    if (source) params.set('score_source', source);
    if (includeSummary) params.set('include_summary', 'true');
    return `/metrics/api/journals?${params}`;
}

/* ------------------------------------------------------------------ */
/* Data loading — server-side pagination                               */
/* ------------------------------------------------------------------ */
let summaryLoaded = false;

async function loadJournalPage(page, includeSummary) {
    const error = document.getElementById('ldr-error');

    try {
        const resp = await fetch(buildApiUrl(page, includeSummary));
        // 503 with "reference database not available" is the expected
        // first-install state, not an error — the data-banner above
        // carries the install CTA. Suppress the red error box so the
        // user's eye goes to the CTA instead of a scary message.
        if (resp.status === 503) {
            error.style.display = 'none';
            document.getElementById('ldr-loading')?.style.setProperty('display', 'none');
            return;
        }
        const result = await resp.json();

        if (result.status !== 'success') throw new Error(result.message || 'Failed');

        const journals = result.journals || [];
        const pag = result.pagination || {};
        currentPage = pag.page || 1;
        totalPages = pag.total_pages || 1;
        totalCount = pag.total_count || 0;

        // Summary stats + charts (only on initial load)
        if (result.summary) {
            const s = result.summary;
            document.getElementById('stat-total').textContent = s.total;
            document.getElementById('stat-avg-quality').textContent = s.avg_quality;
            document.getElementById('stat-predatory').textContent = s.predatory_count;
            document.getElementById('stat-doaj').textContent = s.doaj_count;
            document.getElementById('stat-h-index').textContent = s.avg_h_index ?? '—';

            updateQualityDistChart(s.quality_distribution);
            updateScoreSourceChart(s.source_distribution);
        }

        renderTable(journals, totalCount);
        renderPagination();
        summaryLoaded = true;
    } catch (err) {
        error.textContent = 'Failed to load journal data. ' + err.message;
        error.style.display = 'block';
    }
}

/* ------------------------------------------------------------------ */
/* Charts                                                              */
/* ------------------------------------------------------------------ */
function updateQualityDistChart(dist) {
    if (typeof Chart === 'undefined') return;
    if (typeof Chart.defaults !== 'undefined') Chart.defaults.color = textColor;
    const ctx = document.getElementById('ldr-quality-dist-chart');
    if (qualityDistChart) qualityDistChart.destroy();

    const labels = ['1','2','3','4','5','6','7','8','9','10'];
    const data = labels.map(l => dist[l] || 0);

    qualityDistChart = new Chart(ctx, {
        type: 'bar',
        data: { labels, datasets: [{ label: 'Journals', data, backgroundColor: getBarColors(), borderWidth: 0, borderRadius: 4 }] },
        options: {
            responsive: true, maintainAspectRatio: false,
            scales: { y: { beginAtZero: true }, x: { grid: { display: false } } },
            plugins: { legend: { display: false } }
        }
    });
}

function updateScoreSourceChart(dist) {
    if (typeof Chart === 'undefined') return;
    const ctx = document.getElementById('ldr-score-source-chart');
    if (scoreSourceChart) scoreSourceChart.destroy();

    const labels = Object.keys(dist);
    const data = Object.values(dist);
    const colors = labels.map(l => ({
        openalex: 'rgba(124,92,252,0.8)', doaj: 'rgba(10,207,151,0.8)',
        llm: 'rgba(249,188,11,0.8)', predatory: 'rgba(250,92,124,0.8)',
    }[l] || 'rgba(128,128,128,0.8)'));

    scoreSourceChart = new Chart(ctx, {
        type: 'doughnut',
        data: { labels: labels.map(l => l.charAt(0).toUpperCase() + l.slice(1)), datasets: [{ data, backgroundColor: colors, borderWidth: 0 }] },
        options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }
    });
}

/* ------------------------------------------------------------------ */
/* Your Research — user journal data                                   */
/* ------------------------------------------------------------------ */
async function loadUserResearchJournals() {
    const loading = document.getElementById('ldr-yr-loading');
    const empty = document.getElementById('ldr-yr-empty');
    const content = document.getElementById('ldr-yr-content');

    // If a research_id is in the URL, scope to that single research; otherwise
    // show the cross-research aggregate. The endpoints have the same response
    // shape so the rest of the rendering code is unchanged.
    const researchId = new URLSearchParams(window.location.search).get('research_id');
    const url = researchId
        ? `/metrics/api/journals/research/${encodeURIComponent(researchId)}`
        : '/metrics/api/journals/user-research';

    try {
        const resp = await fetch(url);
        const result = await resp.json();
        if (result.status !== 'success') throw new Error(result.message || 'Failed');

        loading.style.display = 'none';

        const s = result.summary;
        if (s.total_journals === 0) {
            empty.style.display = 'block';
            return;
        }

        document.getElementById('yr-stat-journals').textContent = s.total_journals;
        document.getElementById('yr-stat-avg-quality').textContent =
            s.avg_quality != null ? s.avg_quality : '—';
        document.getElementById('yr-stat-papers').textContent = s.total_papers;
        document.getElementById('yr-stat-predatory').textContent = s.predatory_blocked;
        document.getElementById('yr-table-count').textContent = result.journals.length;

        content.style.display = 'block';
        renderYrQualityChart(result.quality_distribution);
        renderYrSourceChart(result.journals);
        renderYrTable(result.journals);
    } catch (err) {
        loading.style.display = 'none';
        empty.style.display = 'block';
    }
}

function renderYrQualityChart(userDist) {
    if (typeof Chart === 'undefined') return;
    const ctx = document.getElementById('ldr-yr-quality-chart');
    if (yrQualityChart) yrQualityChart.destroy();

    const labels = ['1','2','3','4','5','6','7','8','9','10'];
    const data = labels.map(l => userDist[l] || 0);

    yrQualityChart = new Chart(ctx, {
        type: 'bar',
        data: { labels, datasets: [{ label: 'Your Journals', data, backgroundColor: getBarColors(), borderWidth: 0, borderRadius: 4 }] },
        options: {
            responsive: true, maintainAspectRatio: false,
            scales: { y: { beginAtZero: true, ticks: { precision: 0 } }, x: { grid: { display: false } } },
            plugins: { legend: { display: false } }
        }
    });
}

function renderYrSourceChart(journals) {
    if (typeof Chart === 'undefined' || !journals.length) return;
    const ctx = document.getElementById('ldr-yr-source-chart');
    if (yrSourceChart) yrSourceChart.destroy();

    const counts = {};
    journals.forEach(j => {
        const src = j.score_source || 'unknown';
        counts[src] = (counts[src] || 0) + 1;
    });
    const labels = Object.keys(counts);
    const data = Object.values(counts);
    const colors = labels.map(l => ({
        openalex: 'rgba(124,92,252,0.8)', doaj: 'rgba(10,207,151,0.8)',
        llm: 'rgba(249,188,11,0.8)', predatory: 'rgba(250,92,124,0.8)',
    }[l] || 'rgba(128,128,128,0.8)'));

    yrSourceChart = new Chart(ctx, {
        type: 'doughnut',
        data: { labels: labels.map(l => l.charAt(0).toUpperCase() + l.slice(1)), datasets: [{ data, backgroundColor: colors, borderWidth: 0 }] },
        options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } }
    });
}

function renderYrTable(journals) {
    const tbody = document.getElementById('ldr-yr-tbody');
    if (!journals || journals.length === 0) { tbody.innerHTML = ''; return; }

    tbody.innerHTML = journals.map((j, i) => {
        const t = getQualityTier(j.quality);
        const predClass = j.is_predatory ? ' ldr-row-predatory' : '';
        const rawName = j.name || '';
        const truncName = rawName.length > 50 ? rawName.substring(0, 50) + '...' : rawName;
        return `<tr class="${predClass}">
            <td style="color:var(--text-muted);font-size:0.75rem;">${i + 1}</td>
            <td class="ldr-journal-name" title="${escHtml(rawName)}">${escHtml(truncName)}</td>
            <td><span class="ldr-quality-badge ${t.cls}">${escHtml(String(t.label))}</span></td>
            <td>${getQuartileChip(j.sjr_quartile || j.quartile)}</td>
            <td style="text-align:right;font-weight:600;">${j.paper_count}</td>
            <td style="text-align:center;color:var(--text-muted);font-size:0.85rem;">${formatYearRange(j.year_min, j.year_max)}</td>
            <td>${getSourceBadge(j.score_source)}</td>
            <td><div class="ldr-trust-icons">${getTrustIcons(j)}</div></td>
        </tr>`;
    }).join('');
}

/** Render "2020–2024" or "2023" or "—" from {min, max} year integers. */
function formatYearRange(yMin, yMax) {
    if (yMin == null && yMax == null) return '—';
    if (yMin === yMax || yMin == null || yMax == null) {
        return escHtml(String(yMin ?? yMax));
    }
    return `${escHtml(String(yMin))}\u2013${escHtml(String(yMax))}`;
}

/* ------------------------------------------------------------------ */
/* Table rendering (from server response, no client-side sort/filter)  */
/* ------------------------------------------------------------------ */
function renderTable(journals, total) {
    const tbody = document.getElementById('ldr-journal-tbody');
    document.getElementById('ldr-table-count').textContent = total;

    if (journals.length === 0) {
        tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--text-secondary);padding:2rem;">No journals match your filters</td></tr>';
        document.getElementById('ldr-table-info').textContent = '';
        return;
    }

    tbody.innerHTML = journals.map(j => {
        const t = getQualityTier(j.quality);
        const predClass = j.is_predatory ? ' ldr-row-predatory' : '';
        const rawName = j.name || '';
        const truncName = rawName.length > 45 ? rawName.substring(0, 45) + '...' : rawName;
        return `<tr class="${predClass}">
            <td class="ldr-journal-name" title="${escHtml(rawName)}">${escHtml(truncName)}</td>
            <td><span class="ldr-quality-badge ${t.cls}">${escHtml(String(t.label))}</span></td>
            <td>${getQuartileChip(j.quartile || j.sjr_quartile)}</td>
            <td class="ldr-col-right">${j.h_index != null ? j.h_index.toLocaleString() : '—'}</td>
            <td class="ldr-col-right ldr-col-impact">${j.impact_factor != null ? j.impact_factor.toFixed(2) : '—'}</td>
            <td class="ldr-col-source">${getSourceBadge(j.score_source)}</td>
            <td><div class="ldr-trust-icons">${getTrustIcons(j)}</div></td>
            <td class="ldr-col-publisher" title="${escHtml(j.publisher||'')}">${escHtml((j.publisher||'—').substring(0,30))}</td>
        </tr>`;
    }).join('');

    const start = (currentPage - 1) * PAGE_SIZE + 1;
    const end = start + journals.length - 1;
    document.getElementById('ldr-table-info').textContent = `Showing ${start}–${end} of ${total} journals`;
}

function renderPagination() {
    const container = document.getElementById('ldr-pagination');
    if (totalPages <= 1) { container.innerHTML = ''; return; }

    let html = `<button class="ldr-pagination-btn" onclick="goToPage(${currentPage-1})" ${currentPage===1?'disabled':''}>‹ Prev</button>`;
    const maxButtons = 7;
    let startP = Math.max(1, currentPage - 3);
    let endP = Math.min(totalPages, startP + maxButtons - 1);
    if (endP - startP < maxButtons - 1) startP = Math.max(1, endP - maxButtons + 1);
    for (let i = startP; i <= endP; i++) {
        html += `<button class="ldr-pagination-btn ${i===currentPage?'ldr-active':''}" onclick="goToPage(${i})">${i}</button>`;
    }
    html += `<button class="ldr-pagination-btn" onclick="goToPage(${currentPage+1})" ${currentPage===totalPages?'disabled':''}>Next ›</button>`;
    container.innerHTML = html;
}

function goToPage(page) {
    if (page < 1 || page > totalPages) return;
    loadJournalPage(page, false);
}

/* ------------------------------------------------------------------ */
/* Sort + filter → reload from server                                  */
/* ------------------------------------------------------------------ */
function handleSort(field) {
    if (sortField === field) {
        sortDir = sortDir === 'asc' ? 'desc' : 'asc';
    } else {
        sortField = field;
        sortDir = 'desc';
    }
    document.querySelectorAll('.ldr-journal-table th').forEach(th => {
        th.classList.remove('ldr-sorted-asc', 'ldr-sorted-desc');
        if (th.dataset.sort === sortField) {
            th.classList.add(sortDir === 'asc' ? 'ldr-sorted-asc' : 'ldr-sorted-desc');
        }
    });
    loadJournalPage(1, false);
}

function applyFilters() { loadJournalPage(1, false); }

/* ------------------------------------------------------------------ */
/* Debounce helper                                                     */
/* ------------------------------------------------------------------ */
function debounce(fn, ms) {
    let timer;
    return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), ms); };
}

/* ------------------------------------------------------------------ */
/* Event listeners                                                     */
/* ------------------------------------------------------------------ */
document.getElementById('ldr-journal-search').addEventListener('input', debounce(applyFilters, 300));
document.getElementById('ldr-filter-tier').addEventListener('change', applyFilters);
document.getElementById('ldr-filter-source').addEventListener('change', applyFilters);

document.querySelectorAll('.ldr-journal-table th[data-sort]').forEach(th => {
    th.addEventListener('click', () => handleSort(th.dataset.sort));
});

// Set default sort indicator
document.querySelector('.ldr-journal-table th[data-sort="quality"]').classList.add('ldr-sorted-desc');

// Wait for Vite module scripts (Chart.js) before loading data
window.addEventListener('load', async () => {
    await checkDataStatus();
    // Threshold slider is always visible at the top — load it immediately
    loadThreshold();
    thresholdLoaded = true;
    // Show content container (tabs handle visibility of individual panels)
    document.getElementById('ldr-loading').style.display = 'none';
    document.getElementById('ldr-content').style.display = 'block';
    // Default tab is "Your Research" — when a user opens the dashboard
    // they almost always want to see the journals their own searches
    // touched, not a list of every journal in the bundled DB.
    document.getElementById('ldr-tab-your-research').style.display = '';
    document.getElementById('ldr-tab-global-db').style.display = 'none';
    document.getElementById('ldr-tab-how-it-works').style.display = 'none';
    userResearchLoaded = true;
    loadUserResearchJournals();
});
</script>
{% endblock %}
