/**
 * AegisSight Studio - NotebookLM-artiger Controller.
 *
 * Wiederverwendet die reinen Render-Funktionen aus components.js (UI.*) und den
 * API-Client (API.*). Das globale `App`-Shim stellt nur die wenigen Felder/Handler
 * bereit, die einige geliehene Render-Funktionen per onclick erwarten.
 */

/* --- App-Shim (Minimal-Kompatibilitaet für components.js) --- */
const App = {
    currentIncidentId: null,
    _fcHidden: new Set(),

    // Faktencheck-Statusfilter (von UI.renderFactCheckFilters referenziert)
    toggleFactCheckFilter(status) {
        if (this._fcHidden.has(status)) this._fcHidden.delete(status);
        else this._fcHidden.add(status);
        document.querySelectorAll('#art-fc-body .factcheck-item').forEach(el => {
            el.style.display = this._fcHidden.has(el.dataset.fcStatus) ? 'none' : '';
        });
    },
    toggleFcDropdown(event) {
        if (event) event.stopPropagation();
        const menu = document.getElementById('fc-dropdown-menu');
        if (menu) menu.classList.toggle('open');
    },
    // Stubs (von manchen Render-Funktionen referenziert, hier nicht genutzt)
    toggleSourceOverviewDetail() {},
    syncRefreshStatus() { Studio.syncRefreshStatus(); },
};

/* --- Studio-Controller --- */
const Studio = {
    incidents: [],
    incident: null,
    articles: [],
    sourcesMeta: [],        // Cache von API.listSources() für Badges
    selected: null,         // Set aktiver Quellen-Namen; null = alle aktiv
    convId: null,
    _startTime: null,
    _timerInt: null,
    openTabs: [],           // in der Mitte geöffnete Studio-Teile (Reihenfolge)
    activeTab: null,        // aktuell sichtbarer Tab
    _closeSvg: '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
    // Ereignistypen der Aktivitaets-Timeline: Label + Lucide-Icon
    _evMeta: {
        article_ingest: { label: 'Meldung', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>' },
        refresh:        { label: 'Aktualisierung', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/></svg>' },
        analysis:       { label: 'Analyse', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg>' },
        chat_qa:        { label: 'Frage & Antwort', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>' },
        source_change:  { label: 'Quelle', icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v8"/><path d="M8 12h8"/></svg>' },
    },
    // Untersektionen der Timeline (Reihenfolge + Sektions-Ueberschrift)
    _evSections: [
        { key: 'chat_qa',        label: 'Konversation' },
        { key: 'source_change',  label: 'Quellen' },
        { key: 'refresh',        label: 'Aktualisierungen' },
        { key: 'analysis',       label: 'Analyse' },
        { key: 'article_ingest', label: 'Meldungen' },
    ],

    caseFilter: '',
    caseScope: 'all',       // 'all' | 'mine' - wie die Seitenleiste im klassischen Dashboard
    _me: null,              // Antwort von /auth/me (fuer Konto-Menue und "Eigene")
    _archiveOpen: false,
    caseSel: new Set(),     // Mehrfachauswahl in der Fall-Liste (IDs)
    caseMode: null,         // null = normale Liste, 'archive' | 'delete' = Auswahlmodus

    async init() {
        if (!localStorage.getItem('osint_token')) { window.location.href = '/'; return; }
        try {
            this._me = await API.getMe();
            // Studio ist vorerst nur fuer info@aegis-sight.de freigegeben.
            if (!this._me || this._me.email !== 'info@aegis-sight.de') { window.location.href = '/dashboard'; return; }
        } catch (e) { /* 401 leitet in api.js um */ }
        this._initHeader();
        WS.connect();
        this._wireWs();
        this._initDivider();
        this._initTabDnD();
        await this.loadIncidents();
        const last = localStorage.getItem('studio_incident');
        if (last && this.incidents.some(i => String(i.id) === String(last))) {
            this.selectIncident(last);
        } else {
            // Ohne offenen Fall zeigt die linke Spalte die Fall-Auswahl
            this._showEmpty(true);
            this.leftTab('cases');
        }
    },

    /**
     * Kopfzeile rechts aufbauen. Bewusst derselbe Umfang wie im klassischen
     * Dashboard, damit man beim Wechsel der Ansicht nichts verliert.
     * Barrierefreiheit, Theme-Schalter, Konto-Menue und Abmelden.
     */
    _initHeader() {
        // Barrierefreiheits-Knopf haengt sich selbst vor den Theme-Schalter
        if (window.A11yManager || typeof A11yManager !== 'undefined') A11yManager.init();

        // Theme-Schalter kennt zwei Zustaende ueber seine Klasse
        this._syncThemeSwitch();

        const user = this._me;
        if (!user) return;

        const userEl = document.getElementById('header-user');
        if (userEl) userEl.textContent = user.email || '';

        const orgEl = document.getElementById('header-org-name');
        if (orgEl) orgEl.textContent = user.org_name || '-';

        const licEl = document.getElementById('header-license-info');
        if (licEl) {
            const labels = { trial: 'Trial', annual: 'Jahreslizenz', permanent: 'Permanent' };
            licEl.textContent = user.read_only
                ? 'Abgelaufen'
                : (labels[user.license_type] || user.license_status || '-');
        }

        // Credits nur zeigen, wenn die Lizenz welche fuehrt
        const creditsSection = document.getElementById('credits-section');
        if (creditsSection && user.credits_total) {
            creditsSection.style.display = 'block';
            const percentUsed = user.credits_percent_used || 0;
            const percentRemaining = Math.max(0, 100 - percentUsed);
            const bar = document.getElementById('credits-bar');
            document.getElementById('credits-remaining').textContent = (user.credits_remaining || 0).toLocaleString('de-DE');
            document.getElementById('credits-total').textContent = (user.credits_total || 0).toLocaleString('de-DE');
            bar.style.width = percentRemaining + '%';
            bar.classList.remove('warning', 'critical');
            if (percentUsed > 80) bar.classList.add('critical');
            else if (percentUsed > 50) bar.classList.add('warning');
            const pct = document.getElementById('credits-percent');
            if (pct) pct.textContent = percentRemaining.toFixed(0) + '% verbleibend';
        }

        // Konto-Menue auf- und zuklappen
        const btn = document.getElementById('header-user-btn');
        const dropdown = document.getElementById('header-user-dropdown');
        if (btn && dropdown) {
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                const isOpen = dropdown.classList.toggle('open');
                btn.setAttribute('aria-expanded', String(isOpen));
            });
            dropdown.addEventListener('click', (e) => e.stopPropagation());
            document.addEventListener('click', () => {
                dropdown.classList.remove('open');
                btn.setAttribute('aria-expanded', 'false');
            });
        }
    },

    logout() {
        localStorage.removeItem('osint_token');
        localStorage.removeItem('osint_username');
        localStorage.removeItem('studio_incident');
        WS.disconnect();
        window.location.href = '/';
    },

    /**
     * Ohne offenen Fall bleiben Mitte und rechte Spalte leer - die LINKE Spalte
     * muss aber stehen bleiben, sonst käme man an die Fall-Auswahl nicht heran.
     */
    _showEmpty(on) {
        // Mitte + rechte Spalte bleiben stehen, werden aber ausgegraut und
        // unbedienbar (Klasse 'idle'). Nur der Hinweis legt sich darueber.
        const cols = document.getElementById('studio-cols');
        if (cols) cols.classList.toggle('idle', on);
        document.getElementById('studio-empty').style.display = on ? '' : 'none';
        // Ohne Fall wuerde die Lauf-Anzeige sonst mit dem Stand des zuletzt
        // geoeffneten Falls stehen bleiben. Nur beim Leerschalten neu zeichnen,
        // beim Oeffnen macht das loadDetail mit den richtigen Daten.
        if (on) {
            this.incident = null;
            this.fresh = null;
            this._renderLauf();
        }
    },

    // === Linke Spalte: Reiter "Fälle" / "Quellen" ===
    leftTab(which) {
        const cases = which === 'cases';
        // Beim Verlassen der Fall-Liste den Auswahlmodus nicht offen lassen
        if (!cases && this.caseMode !== null) this.endCaseMode();
        document.getElementById('pane-cases').hidden = !cases;
        document.getElementById('pane-sources').hidden = cases;
        const tc = document.getElementById('lt-cases');
        const ts = document.getElementById('lt-sources');
        tc.classList.toggle('active', cases);
        ts.classList.toggle('active', !cases);
        tc.setAttribute('aria-selected', String(cases));
        ts.setAttribute('aria-selected', String(!cases));
    },

    // === Fall-Liste (früher das Dropdown in der Kopfzeile) ===
    async loadIncidents() {
        try {
            this.incidents = await API.listIncidents() || [];
        } catch (e) {
            UI.showToast('Lagen konnten nicht geladen werden', 'error');
            this.incidents = [];
        }
        this.renderCases();
    },

    filterCases(q) {
        this.caseFilter = (q || '').trim().toLowerCase();
        this.renderCases();
    },

    // "Alle" oder nur die selbst angelegten Fälle, wie in der Seitenleiste
    // des klassischen Dashboards.
    setCaseScope(scope) {
        this.caseScope = scope === 'mine' ? 'mine' : 'all';
        document.querySelectorAll('.case-scope .sidebar-filter-btn').forEach(btn => {
            const on = btn.dataset.scope === this.caseScope;
            btn.classList.toggle('active', on);
            btn.setAttribute('aria-pressed', String(on));
        });
        this.renderCases();
    },

    // Trifft der Fall den aktuellen Suchtext und die Auswahl "Alle"/"Eigene"?
    _caseMatches(i) {
        const q = this.caseFilter;
        if (q && !(i.title || '').toLowerCase().includes(q)) return false;
        if (this.caseScope === 'mine') {
            const me = this._me && this._me.email;
            if (!me || i.created_by_username !== me) return false;
        }
        return true;
    },

    toggleArchive() {
        this._archiveOpen = !this._archiveOpen;
        this.renderCases();
    },

    // Fälle, die der aktuelle Filter zeigt (Archiv nur, wenn aufgeklappt) - genau
    // diese Menge trifft "Alle auswählen".
    _visibleCases() {
        return this.incidents.filter(i => this._caseMatches(i) && (i.status === 'active' || this._archiveOpen));
    },

    renderCases() {
        const list = document.getElementById('cases-list');
        if (!list) return;
        const all = this.incidents.filter(i => this._caseMatches(i));
        const cur = this.incident ? String(this.incident.id) : null;

        const groups = [
            { key: 'live', label: 'Live-Monitoring', items: all.filter(i => i.type !== 'research' && i.status === 'active') },
            { key: 'research', label: 'Recherchen', items: all.filter(i => i.type === 'research' && i.status === 'active') },
        ];
        const archived = all.filter(i => i.status !== 'active');

        // Haken und Kontextmenue schließen sich aus: im Auswahlmodus häkelt man,
        // sonst öffnet ein Klick den Fall.
        const sel = this.caseMode !== null;
        const item = (i) => `
            <div class="case-item${String(i.id) === cur ? ' active' : ''}${this.caseSel.has(i.id) ? ' picked' : ''}" data-id="${i.id}">
                ${sel ? `<label class="case-check" title="Auswählen">
                    <input type="checkbox" ${this.caseSel.has(i.id) ? 'checked' : ''}
                           onchange="Studio.toggleCaseSel(${i.id}, this.checked)">
                </label>` : ''}
                <button class="case-open" onclick="${sel ? `Studio.toggleCaseSel(${i.id}, !Studio.caseSel.has(${i.id}))` : `Studio.selectIncident(${i.id})`}" title="${UI.escape(i.title)}">
                    <span class="case-title">${UI.escape(i.title)}</span>
                    ${Studio._aiTag(i, 'case-ai-tag')}
                    <span class="case-count">${i.article_count}</span>
                </button>
                ${sel ? '' : `<button class="case-menu-btn" onclick="Studio.caseMenu(event, ${i.id})" title="Mehr" aria-label="Mehr">
                    <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="12" cy="19" r="1"/></svg>
                </button>`}
            </div>`;

        let html = '';
        groups.forEach(g => {
            if (!g.items.length) return;
            html += `<div class="case-group">${g.label} <span>${g.items.length}</span></div>`;
            html += g.items.map(item).join('');
        });
        if (archived.length) {
            html += `<button class="case-group case-group-toggle${this._archiveOpen ? ' open' : ''}" onclick="Studio.toggleArchive()">
                        <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
                        Archiv <span>${archived.length}</span>
                     </button>`;
            if (this._archiveOpen) html += archived.map(item).join('');
        }
        list.innerHTML = html;   // ohne Treffer bleibt die Liste bewusst leer (kein Hinweistext)

        // Zähler am Reiter zeigt, was die Liste gerade zeigt (also inkl. "Eigene")
        const cnt = document.getElementById('cases-count');
        if (cnt) cnt.textContent = all.filter(i => i.status === 'active').length || '';
        this._renderBulkBar();
    },

    /**
     * Rueckfrage im Studio-Design statt Browser-confirm(). Gibt ein Promise<boolean>.
     * (Das native confirm sieht nach Browser-Warnung aus und blockiert die Seite.)
     */
    _confirmResolve: null,
    askConfirm({ title, body, okLabel = 'Löschen', danger = true }) {
        return new Promise((resolve) => {
            this._confirmResolve = resolve;
            document.getElementById('confirm-title').textContent = title;
            document.getElementById('confirm-body').innerHTML = body;
            const ok = document.getElementById('confirm-ok');
            ok.textContent = okLabel;
            ok.className = 'btn ' + (danger ? 'btn-danger' : 'btn-primary');
            document.getElementById('modal-confirm').classList.add('active');
            setTimeout(() => document.getElementById('confirm-cancel').focus(), 50);
        });
    },
    _confirmClose(answer) {
        document.getElementById('modal-confirm').classList.remove('active');
        const r = this._confirmResolve;
        this._confirmResolve = null;
        if (r) r(answer);
    },

    /* === Startpanel: der frisch angelegte Fall fragt aktiv nach ===
       Früher stand der Nutzer nach dem Anlegen vor einer leeren Oberflaeche und
       musste die Pipeline kennen, um irgendetwas auszuloesen. Jetzt legt das Studio
       die Wege offen hin - "später entscheiden" ist einer davon, aber eine bewusste
       Wahl. Ein Fall, für den schon einmal gewählt wurde, fragt nicht erneut. */
    _renderStartPanel() {
        const panel = document.getElementById('start-panel');
        if (!panel) return;
        const inc = this.incident;
        const leer = inc && !(this.articles || []).length && !((inc.summary || '').trim());
        const schonGewaehlt = inc && localStorage.getItem('studio_start_' + inc.id);
        const zeigen = !!leer && !this._runningStage && !schonGewaehlt;
        panel.hidden = !zeigen;

        // Der Stand-Kasten und die Startfrage sind dieselbe Aktion, nie beide
        // gleichzeitig zeigen. Wird die Frage nicht mehr gestellt, ist der Kasten
        // wieder da, sonst stuende man nach einem ergebnislosen Lauf ohne
        // Startmoeglichkeit da. Die Kette darunter bleibt sichtbar, sie erklaert,
        // was der Startdialog anbietet.
        const runAll = document.getElementById('lauf-stand');
        if (runAll) runAll.hidden = zeigen;

        if (!zeigen) return;

        // Texte an den Lage-Typ anpassen: eine Recherche läuft beim ersten Mal
        // mit drei Durchläufen und dauert entsprechend laenger.
        const research = inc.type === 'research';
        const full = document.getElementById('sp-full-desc');
        if (full) {
            full.textContent = research
                ? 'Tiefenrecherche in drei Durchläufen (Breite Erfassung, Vertiefung, Konsolidierung), danach Analyse und Faktencheck. Dauert deutlich länger als ein einzelner Lauf.'
                : 'Artikel holen, Lagebild schreiben, Fakten prüfen und Orte erkennen, nacheinander. Am Ende steht ein fertiges Lagebild.';
        }
        const auto = document.getElementById('sp-auto-desc');
        if (auto) {
            auto.textContent = inc.refresh_mode === 'auto'
                ? 'Ist für diesen Fall bereits eingeschaltet - der erste Lauf startet von selbst innerhalb einer Minute.'
                : `Der Fall läuft ab jetzt selbstständig (alle ${inc.refresh_interval || 15} Minuten). Der erste Lauf beginnt sofort.`;
        }
        this._checkSearchStatus();
    },

    // Ehrlich ansagen, wenn ein Lauf gerade kaum etwas bringen kann
    async _checkSearchStatus() {
        const box = document.getElementById('sp-warn');
        const txt = document.getElementById('sp-warn-text');
        if (!box) return;
        let st = null;
        try { st = await API.getSearchStatus(); } catch (e) { return; }
        const blind = !st || !st.reachable || !st.search_enabled;
        box.hidden = !blind;
        if (blind && txt) {
            txt.textContent = (!st || !st.reachable)
                ? 'Die Websuche antwortet gerade nicht (Suchdienst nicht erreichbar). Ein Lauf bringt derzeit nur Treffer aus den RSS-Feeds.'
                : 'Die Websuche ist abgeschaltet, weil kein mobiler Ausgang steht. Ein Lauf bringt derzeit nur Treffer aus den RSS-Feeds.';
        }
    },

    async startChoice(kind) {
        const panel = document.getElementById('start-panel');
        const inc = this.incident;
        if (!inc) return;

        // Eine Wahl -> dieser Fall fragt nicht noch einmal, und der Startknopf rechts
        // kommt zurück (beide sind dieselbe Aktion).
        localStorage.setItem('studio_start_' + inc.id, kind);
        panel.hidden = true;
        const runAll = document.getElementById('lauf-stand');
        if (runAll) runAll.hidden = false;

        if (kind === 'ingest') {
            this.leftTab('sources');
            const ip = document.getElementById('ingest-panel');
            if (ip && ip.hidden) this.toggleIngest();
            document.getElementById('ingest-add-btn').focus();
            return;
        }
        if (kind === 'auto') {
            try {
                await API.updateIncident(inc.id, { refresh_mode: 'auto' });
                inc.refresh_mode = 'auto';
                await this.loadIncidents();
                UI.showToast('Automatische Aktualisierung eingeschaltet - der erste Lauf startet gleich.', 'success');
            } catch (e) {
                UI.showToast((e && e.message) || 'Umstellen fehlgeschlagen', 'error');
                localStorage.removeItem('studio_start_' + inc.id);
                panel.hidden = false;
            }
            return;
        }
        // Bleibt 'full'. Einzelne Schritte lassen sich nicht mehr starten.
        this.runStage('full');
    },

    // === Auswahlmodus + Massenaktionen ===
    // Die Haken sind normalerweise WEG. Erst der Knopf "Archivieren" bzw. "Löschen"
    // unten schaltet den Auswahlmodus ein und blendet sie ein.
    startCaseMode(mode) {
        this.caseMode = mode;
        this.caseSel.clear();
        this.renderCases();
    },
    endCaseMode() {
        this.caseMode = null;
        this.caseSel.clear();
        const sa = document.getElementById('case-select-all');
        if (sa) sa.checked = false;
        this.renderCases();
    },

    toggleCaseSel(id, on) {
        if (on) this.caseSel.add(id); else this.caseSel.delete(id);
        this.renderCases();
    },

    selectAllCases(on) {
        const vis = this._visibleCases();
        if (on) vis.forEach(i => this.caseSel.add(i.id));
        else vis.forEach(i => this.caseSel.delete(i.id));
        this.renderCases();
    },

    _renderBulkBar() {
        const mode = this.caseMode;
        const n = this.caseSel.size;

        // Standardleiste (zwei Startknoepfe) vs. Auswahlmodus (bestätigen/abbrechen)
        const tools = document.getElementById('case-tools');
        const bar = document.getElementById('bulk-bar');
        const row = document.getElementById('case-select-row');
        if (tools) tools.hidden = mode !== null;
        if (bar) bar.hidden = mode === null;
        if (row) row.hidden = mode === null;
        if (mode === null) return;

        const cnt = document.getElementById('bulk-count');
        if (cnt) {
            cnt.textContent = n === 0
                ? (mode === 'delete' ? 'Fälle zum Löschen anhaken' : 'Fälle zum Archivieren anhaken')
                : (n === 1 ? '1 Fall ausgewählt' : `${n} Fälle ausgewählt`);
        }

        // Nur anbieten, was zum Modus UND zur Auswahl passt
        const sel = this.incidents.filter(i => this.caseSel.has(i.id));
        const anyActive = sel.some(i => i.status === 'active');
        const anyArchived = sel.some(i => i.status !== 'active');
        const ba = document.getElementById('bulk-archive');
        const bv = document.getElementById('bulk-activate');
        const bd = document.getElementById('bulk-delete');
        // Beschriftung in die span, sonst wuerde textContent das Icon entfernen.
        const setzeText = (el, text) => {
            if (!el) return;
            const ziel = el.querySelector('span');
            if (ziel) ziel.textContent = text; else el.textContent = text;
        };
        if (ba) { ba.hidden = mode !== 'archive' || !anyActive; setzeText(ba, `Archivieren (${sel.filter(i => i.status === 'active').length})`); }
        if (bv) { bv.hidden = mode !== 'archive' || !anyArchived; setzeText(bv, `Aktivieren (${sel.filter(i => i.status !== 'active').length})`); }
        if (bd) { bd.hidden = mode !== 'delete' || n === 0; setzeText(bd, `Löschen (${n})`); }

        // "Alle"-Haken spiegelt, ob die sichtbare Menge komplett gewählt ist
        const vis = this._visibleCases();
        const sa = document.getElementById('case-select-all');
        if (sa) sa.checked = vis.length > 0 && vis.every(i => this.caseSel.has(i.id));

        const hint = document.getElementById('case-sel-hint');
        if (hint) hint.textContent = n ? `${n} ausgewählt` : '';
    },

    // Massenaktionen laufen sequentiell über die normalen Endpunkte (mit deren
    // Rechte-Pruefung). Fehlschlaege werden gezählt statt still verschluckt.
    async _bulkRun(ids, fn, verb) {
        let ok = 0;
        const failed = [];
        for (const id of ids) {
            try { await fn(id); ok++; }
            catch (e) { failed.push(id); }
        }
        this.caseSel.clear();
        this.caseMode = null;          // Auswahlmodus verlassen -> Haken verschwinden wieder
        await this.loadIncidents();
        if (failed.length) {
            UI.showToast(`${ok} ${verb}, ${failed.length} fehlgeschlagen (IDs ${failed.join(', ')})`, 'error');
        } else {
            UI.showToast(`${ok} ${ok === 1 ? 'Fall' : 'Fälle'} ${verb}`, 'success');
        }
    },

    async bulkStatus(status) {
        const ids = this.incidents
            .filter(i => this.caseSel.has(i.id) && (status === 'archived' ? i.status === 'active' : i.status !== 'active'))
            .map(i => i.id);
        if (!ids.length) return;
        const verb = status === 'archived' ? 'archiviert' : 'wieder aktiviert';
        await this._bulkRun(ids, (id) => API.updateIncident(id, { status }), verb);
    },

    async bulkDelete() {
        const sel = this.incidents.filter(i => this.caseSel.has(i.id));
        if (!sel.length) return;
        const arts = sel.reduce((n, i) => n + (i.article_count || 0), 0);
        const liste = sel.slice(0, 8).map(i => `<li>${UI.escape(i.title)}</li>`).join('')
            + (sel.length > 8 ? `<li class="cd-more">… und ${sel.length - 8} weitere</li>` : '');
        const ok = await this.askConfirm({
            title: `${sel.length} ${sel.length === 1 ? 'Fall' : 'Fälle'} endgültig löschen?`,
            okLabel: `Löschen (${sel.length})`,
            body: `<ul class="confirm-list">${liste}</ul>
                   <p class="confirm-warn">Damit gehen <strong>${arts} Artikel</strong> samt Faktenchecks,
                   Lageberichten und Karten verloren. Das lässt sich nicht rückgängig machen.</p>
                   <p class="confirm-hint">Wenn du sie nur aus der Liste nehmen willst, nutze stattdessen „Archivieren“.</p>`,
        });
        if (!ok) return;

        const ids = sel.map(i => i.id);
        const offen = this.incident && ids.includes(this.incident.id);
        await this._bulkRun(ids, (id) => API.deleteIncident(id), 'gelöscht');
        if (offen) {
            // Der offene Fall war dabei -> zurück in die leere Ansicht
            this.incident = null;
            localStorage.removeItem('studio_incident');
            this._showEmpty(true);
            this.leftTab('cases');
            document.getElementById('incident-title').textContent = '';
            document.getElementById('type-badge').style.display = 'none';
        }
    },

    // Kontextmenue je Fall: archivieren / reaktivieren / löschen
    caseMenu(ev, id) {
        ev.stopPropagation();
        this._closeCaseMenu();
        const inc = this.incidents.find(i => String(i.id) === String(id));
        if (!inc) return;
        const archived = inc.status !== 'active';
        const m = document.createElement('div');
        m.className = 'case-menu';
        m.id = 'case-menu';
        m.innerHTML = `
            <button onclick="Studio.setCaseStatus(${id}, '${archived ? 'active' : 'archived'}')">
                ${archived ? 'Wieder aktivieren' : 'Archivieren'}
            </button>
            <button class="danger" onclick="Studio.deleteCase(${id})">Löschen …</button>`;
        document.body.appendChild(m);
        const r = ev.currentTarget.getBoundingClientRect();
        m.style.top = `${Math.min(r.bottom + 4, window.innerHeight - m.offsetHeight - 8)}px`;
        m.style.left = `${Math.min(r.left, window.innerWidth - m.offsetWidth - 8)}px`;
        this._caseMenuCloser = () => this._closeCaseMenu();
        setTimeout(() => document.addEventListener('click', this._caseMenuCloser, { once: true }), 0);
    },
    _closeCaseMenu() {
        const m = document.getElementById('case-menu');
        if (m) m.remove();
        if (this._caseMenuCloser) {
            document.removeEventListener('click', this._caseMenuCloser);
            this._caseMenuCloser = null;
        }
    },

    async setCaseStatus(id, status) {
        this._closeCaseMenu();
        try {
            await API.updateIncident(id, { status });
            await this.loadIncidents();
            UI.showToast(status === 'archived' ? 'Fall archiviert' : 'Fall wieder aktiv', 'info');
        } catch (e) {
            UI.showToast((e && e.message) || 'Änderung fehlgeschlagen', 'error');
        }
    },

    async deleteCase(id) {
        this._closeCaseMenu();
        const inc = this.incidents.find(i => String(i.id) === String(id));
        const name = inc ? inc.title : 'dieser Fall';
        const arts = inc ? (inc.article_count || 0) : 0;
        const ok = await this.askConfirm({
            title: 'Fall endgültig löschen?',
            okLabel: 'Löschen',
            body: `<ul class="confirm-list"><li>${UI.escape(name)}</li></ul>
                   <p class="confirm-warn">Damit gehen <strong>${arts} Artikel</strong> samt Faktenchecks,
                   Lageberichten und Karten verloren. Das lässt sich nicht rückgängig machen.</p>
                   <p class="confirm-hint">Wenn du ihn nur aus der Liste nehmen willst, nutze stattdessen „Archivieren“.</p>`,
        });
        if (!ok) return;
        try {
            await API.deleteIncident(id);
            if (this.incident && String(this.incident.id) === String(id)) {
                this.incident = null;
                localStorage.removeItem('studio_incident');
                this._showEmpty(true);
                this.leftTab('cases');
                document.getElementById('incident-title').textContent = '';
                document.getElementById('type-badge').style.display = 'none';
            }
            await this.loadIncidents();
            UI.showToast('Fall gelöscht', 'info');
        } catch (e) {
            UI.showToast((e && e.message) || 'Löschen fehlgeschlagen', 'error');
        }
    },

    // === Auswahl + Detail laden ===
    async selectIncident(id) {
        id = parseInt(id);
        if (!id) return;
        App.currentIncidentId = id;
        localStorage.setItem('studio_incident', id);
        this._showEmpty(false);
        this.selected = null;
        this.convId = null;
        const si = document.getElementById('src-search-input');
        if (si) si.value = '';
        // Fall gewählt -> es geht mit den Quellen weiter
        this.leftTab('sources');
        await this.loadDetail(id);
        this.renderCases();     // aktiven Fall in der Liste hervorheben
    },

    async loadDetail(id) {
        let inc, arts, fcs, locs, srcMeta, citeSrc, fresh;
        try {
            [inc, arts, fcs, locs, srcMeta, citeSrc, fresh] = await Promise.all([
                API.getIncident(id),
                API.getArticles(id, { limit: 500 }).catch(() => ({ articles: [] })),
                API.getFactChecks(id).catch(() => []),
                API.getLocations(id).catch(() => []),
                this.sourcesMeta.length ? Promise.resolve(this.sourcesMeta) : API.listSources().catch(() => []),
                API.getIncidentSources(id).catch(() => ({ sources: [] })),
                API.getFreshness(id).catch(() => null),
            ]);
        } catch (e) {
            UI.showToast('Lage-Details konnten nicht geladen werden', 'error');
            return;
        }
        this.fresh = fresh;   // Datenstand je Artefakt (Grundlage der Veraltet-Anzeige)
        this.incident = inc;
        this.articles = Array.isArray(arts) ? arts : (arts.articles || []);
        this.sourcesMeta = Array.isArray(srcMeta) ? srcMeta : [];
        // /locations antwortet mit {locations, category_labels}, nicht mit einem Array.
        // Ungeprüft weitergereicht wirft UI.renderMap (locations.reduce) und bricht
        // loadDetail ab. Gleiche Normalisierung wie im Dashboard (app.js).
        const locList = Array.isArray(locs) ? locs : ((locs && locs.locations) || []);
        const catLabels = (locs && !Array.isArray(locs) && locs.category_labels) || null;
        // Zitat-Quellen (getIncident liefert sources_json bewusst nicht mit)
        this.citeSources = (citeSrc && citeSrc.sources) || [];

        this.renderHeader(inc);
        this.renderSources();
        // Quellen-Ingest: Zustand je Fall zurücksetzen + laufende/fertige Uploads laden
        this._stopUploadPoll();
        this._seenDone = new Set();
        this.uploads = [];
        const ip = document.getElementById('ingest-panel'); if (ip) ip.hidden = true;
        this.loadUploads();
        this.renderArtifacts(inc, fcs, locList, catLabels);
        this._stagesIdle();         // Karten freischalten + Datenstand anzeigen
        this._resumeStage();        // läuft gerade ein Baustein? -> Karte zurück auf "läuft"
        this._renderStartPanel();   // leerer Fall? -> aktiv nach dem nächsten Schritt fragen
        this.renderChatIntro(inc);
        // Snapshots lazy nachladen
        this._snapLoaded = false;
        // Studio-Tabs zurücksetzen und Lagebild als Standard-Tab öffnen
        this._initTabs();
    },

    _sources(inc) {
        // Zitat-Quellen aus /incidents/{id}/sources (getIncident liefert sie nicht mit)
        return this.citeSources || [];
    },
    _isResearch() { return this.incident && this.incident.type === 'research'; },

    renderHeader(inc) {
        const badge = document.getElementById('type-badge');
        const research = inc.type === 'research';
        // Der Titel stand früher im Dropdown - ohne ihn wüsste man nicht, worin man arbeitet
        const t = document.getElementById('incident-title');
        if (t) {
            t.textContent = inc.title || '';
            t.title = inc.title || '';
        }
        badge.textContent = research ? 'Analyse' : 'Live';
        badge.className = 'studio-type-badge' + (research ? ' type-research' : '');
        badge.style.display = '';
        // KI-Weg nur zeigen, wenn der Fall ausdruecklich darauf steht. Ohne
        // Angabe gilt die Vorgabe der Organisation, dann bleibt es unbeschriftet.
        const aiBadge = document.getElementById('studio-ai-badge');
        if (aiBadge) aiBadge.innerHTML = this._aiTag(inc, 'incident-ai-badge');
        document.getElementById('art-summary-title').textContent = research ? 'Recherchebericht' : 'Lagebild';
        document.getElementById('art-latest-title').textContent = research ? 'Zusammenfassung' : 'Neueste Entwicklungen';
    },

    // === Spalte 1: Quellen ===
    _norm(s) { return (s || '').toLowerCase().replace(/^(der|die|das)\s+/, '').replace(/\s+/g, ' ').trim(); },

    _domainOf(url) {
        try { return new URL(url).hostname.replace(/^www\./, '').toLowerCase(); }
        catch (e) { return ''; }
    },
    _reg(dom) { return (dom || '').split('.').slice(-2).join('.'); },

    _feedFor(sourceName, sampleUrl) {
        // 1. Domain-Match (zuverlässig) — exakter Host, dann registrierbare Domain.
        //    KEIN Fuzzy-Namensmatch: "Bundestag" würde sonst fälschlich die
        //    Telegram-Quelle "AfD-Fraktion im Bundestag" (niedrig) treffen.
        const dom = this._domainOf(sampleUrl);
        if (dom) {
            let f = this.sourcesMeta.find(s => s.domain && s.domain.toLowerCase() === dom);
            if (f) return f;
            const reg = this._reg(dom);
            f = this.sourcesMeta.find(s => s.domain && this._reg(s.domain.toLowerCase()) === reg);
            if (f) return f;
        }
        // 2. Exakter (normalisierter) Namensmatch als Fallback — kein Teilstring.
        const n = this._norm(sourceName);
        if (!n) return null;
        return this.sourcesMeta.find(s => this._norm(s.name) === n) || null;
    },

    renderSources() {
        const list = document.getElementById('sources-list');
        // Fertige Uploads je Artikel -> Kategorie + Aktionen
        const upByArt = {};
        (this.uploads || []).forEach(u => { if (u.status === 'done' && u.article_id) upByArt[u.article_id] = u; });

        // Artikel nach Quelle gruppieren; Quelle einer Kategorie zuordnen
        // (reguläre/RSS-Quellen -> "webseiten", hinzugefügte Materialien -> ihre Kategorie)
        const groups = {};
        this.articles.forEach(a => {
            const name = a.source || 'Unbekannt';
            const up = upByArt[a.id];
            if (!groups[name]) groups[name] = { name, arts: [], langs: new Set(), cat: 'webseiten' };
            groups[name].arts.push(a);
            groups[name].langs.add((a.language || 'de').toUpperCase());
            if (up) groups[name].cat = up.category || 'dokumente';
        });
        const arr = Object.values(groups);

        // Im Reiter ist nur Platz für die Zahl; das Ausführliche steckt im Tooltip
        const sc = document.getElementById('sources-count');
        sc.textContent = arr.length || '';
        sc.title = `${this.articles.length} Artikel aus ${arr.length} Quellen`;

        if (!arr.length) {
            list.innerHTML = '<div class="empty-hint">Noch keine Artikel. Starte eine Aktualisierung.</div>';
            return;
        }

        // nach Kategorie gliedern (feste Reihenfolge), innerhalb nach Artikelzahl
        const order = ['webseiten', 'dokumente', 'bilder', 'sprachnachrichten'];
        const byCat = {};
        arr.forEach(g => { (byCat[g.cat] = byCat[g.cat] || []).push(g); });
        let html = '';
        order.filter(c => byCat[c]).forEach(cat => {
            const meta = this._ingCat[cat] || { label: cat, icon: '' };
            const gs = byCat[cat].sort((a, b) => b.arts.length - a.arts.length);
            const nArts = gs.reduce((n, g) => n + g.arts.length, 0);
            const collapsed = this._collapsedCats && this._collapsedCats.has(cat);
            html += `<div class="src-cat${collapsed ? ' collapsed' : ''}" data-cat="${cat}">
                <div class="src-cat-head" onclick="Studio.toggleCat('${cat}')">
                    <svg class="src-cat-chevron" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
                    ${meta.icon}<span>${meta.label}</span><span class="src-cat-count">${nArts}</span>
                </div>`;
            html += gs.map(g => this._renderSourceGroup(g, upByArt)).join('');
            html += `</div>`;
        });
        list.innerHTML = html;
    },
    _renderSourceGroup(g, upByArt) {
        const feed = this._feedFor(g.name, g.arts[0] && g.arts[0].source_url);
        const badges = feed ? UI._renderClassificationBadges(feed) : '';
        const nameEsc = UI.escape(g.name);
        const arts = g.arts
            .slice()
            .sort((a, b) => this._ts(b) - this._ts(a))
            .slice(0, 30)
            .map(a => {
                const t = this._fmtDate(a);
                const h = UI.escape(a.headline_de || a.headline || a.title || 'Ohne Titel');
                const up = upByArt[a.id];
                if (up && up.category !== 'webseiten') {
                    // Datei-Quelle (Dokument/Bild/Audio): Aktionen statt Link
                    return `<div class="src-article src-upload">
                        <div><span class="src-article-date">${t}</span>${h}</div>
                        <div class="ju-actions">${this._uploadActions(up)}
                            <button class="ju-btn ju-btn-del" onclick="Studio.deleteUploadItem(${up.id})">Entfernen</button>
                        </div>
                        <div class="ju-media" id="ju-media-${up.id}"></div>
                    </div>`;
                }
                return a.source_url
                    ? `<a class="src-article" href="${UI.escape(a.source_url)}" target="_blank" rel="noopener"><span class="src-article-date">${t}</span>${h}</a>`
                    : `<div class="src-article"><span class="src-article-date">${t}</span>${h}</div>`;
            }).join('');
        return `<div class="src-item open" data-source="${nameEsc}">
            <div class="src-head">
                <input type="checkbox" class="src-check" checked onclick="event.stopPropagation();Studio.toggleSource('${nameEsc.replace(/'/g, "\\'")}', this.checked)">
                <span class="src-name" onclick="Studio.toggleSrcOpen(this)" title="${nameEsc}">${nameEsc}</span>
                <span class="src-badges">${badges}</span>
                <span class="src-lang">${[...g.langs].join('/')}</span>
                <span class="src-count">${g.arts.length}</span>
            </div>
            <div class="src-articles">${arts}</div>
        </div>`;
    },

    toggleSrcOpen(nameEl) {
        const item = nameEl.closest('.src-item');
        if (item) item.classList.toggle('open');
    },
    toggleCat(cat) {
        if (!this._collapsedCats) this._collapsedCats = new Set();
        const el = document.querySelector(`.src-cat[data-cat="${cat}"]`);
        if (this._collapsedCats.has(cat)) { this._collapsedCats.delete(cat); if (el) el.classList.remove('collapsed'); }
        else { this._collapsedCats.add(cat); if (el) el.classList.add('collapsed'); }
    },
    toggleSource(name, on) {
        if (this.selected === null) {
            // bisher alle aktiv -> Set aus allen Namen bilden
            this.selected = new Set(Object.values(this.articles.reduce((m, a) => {
                m[a.source || 'Unbekannt'] = a.source || 'Unbekannt'; return m;
            }, {})));
        }
        if (on) this.selected.add(name); else this.selected.delete(name);
        const item = document.querySelector(`.src-item[data-source="${CSS.escape(name)}"]`);
        if (item) item.classList.toggle('deselected', !on);
        // "Alle"-Checkbox synchronisieren
        const total = document.querySelectorAll('.src-item').length;
        document.getElementById('src-select-all').checked = this.selected.size >= total;
    },
    toggleAllSources(on) {
        document.querySelectorAll('.src-item').forEach(item => {
            item.classList.toggle('deselected', !on);
            const cb = item.querySelector('.src-check');
            if (cb) cb.checked = on;
        });
        if (on) { this.selected = null; }
        else { this.selected = new Set(); }
    },
    _activeSourceFilter() {
        if (this.selected === null) return null;   // alle
        return [...this.selected];
    },

    // Live-Filter der Quellenliste (Name + Artikel-Ueberschriften)
    filterSources(q) {
        const term = (q || '').trim().toLowerCase();
        document.querySelectorAll('#sources-list .src-item').forEach(item => {
            const name = (item.dataset.source || '').toLowerCase();
            let match = !term || name.includes(term);
            if (!match) {
                const arts = item.querySelectorAll('.src-article');
                for (const a of arts) {
                    if (a.textContent.toLowerCase().includes(term)) { match = true; break; }
                }
            }
            item.classList.toggle('filtered-out', !match);
        });
    },

    // === Quellen-Ingest (Upload/URL -> Artikel) ===
    _ingCat: {
        webseiten:        { label: 'Webseiten',        icon: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>' },
        dokumente:        { label: 'Dokumente',        icon: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>' },
        bilder:           { label: 'Bilder',           icon: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>' },
        sprachnachrichten:{ label: 'Sprachnachrichten', icon: '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/></svg>' },
    },
    toggleIngest() {
        const p = document.getElementById('ingest-panel');
        if (p) p.hidden = !p.hidden;
    },
    dzOver(e) { e.preventDefault(); document.getElementById('dropzone').classList.add('drag'); },
    dzLeave(e) { e.preventDefault(); document.getElementById('dropzone').classList.remove('drag'); },
    dzDrop(e) {
        e.preventDefault();
        document.getElementById('dropzone').classList.remove('drag');
        const files = e.dataTransfer && e.dataTransfer.files;
        if (files && files.length) this.dzFiles(files);
    },
    dzFiles(fileList) {
        const files = Array.from(fileList || []);
        if (files.length) this._submitUpload(files, null);
    },
    urlKey(e) { if (e.key === 'Enter') { e.preventDefault(); this.addUrl(); } },
    addUrl() {
        const inp = document.getElementById('ingest-url');
        const u = (inp.value || '').trim();
        if (!u) return;
        if (!/^https?:\/\//i.test(u)) { UI.showToast('URL muss mit http:// oder https:// beginnen', 'error'); return; }
        inp.value = '';
        this._submitUpload([], u);
    },
    async _submitUpload(files, url) {
        if (!this.incident) return;
        try {
            await API.createUploads(this.incident.id, { files, url });
            await this.loadUploads();
            this._startUploadPoll();
        } catch (e) {
            UI.showToast((e && e.message) || 'Hinzufügen fehlgeschlagen', 'error');
        }
    },
    async loadUploads() {
        if (!this.incident) return;
        let res;
        try { res = await API.listUploads(this.incident.id); }
        catch (e) { return; }
        const prevDone = new Set((this.uploads || []).filter(u => u.status === 'done').map(u => u.id));
        this.uploads = (res && res.uploads) || [];
        this.renderUploads();
        // Frisch fertige Jobs -> neue Artikel in die Quellenliste übernehmen
        const freshlyDone = this.uploads.some(u => u.status === 'done' && !prevDone.has(u.id) && !this._seenDone.has(u.id));
        this.uploads.forEach(u => { if (u.status === 'done') this._seenDone.add(u.id); });
        if (freshlyDone) this._refreshCorpus();
        const active = this.uploads.some(u => u.status === 'queued' || u.status === 'processing');
        if (active) this._startUploadPoll(); else this._stopUploadPoll();
    },
    _startUploadPoll() {
        if (this._uploadPoll) return;
        this._uploadPoll = setInterval(() => this.loadUploads(), 1800);
    },
    _stopUploadPoll() {
        if (this._uploadPoll) { clearInterval(this._uploadPoll); this._uploadPoll = null; }
    },
    async _refreshCorpus() {
        try {
            const arts = await API.getArticles(this.incident.id, { limit: 500 }).catch(() => ({ articles: [] }));
            this.articles = Array.isArray(arts) ? arts : (arts.articles || []);
            this.renderSources();
        } catch (e) { /* nicht kritisch */ }
    },
    renderUploads() {
        const host = document.getElementById('ingest-jobs');
        if (!host) return;
        // Nur laufende/fehlerhafte Jobs hier; fertige erscheinen in der Quellenliste.
        const ups = (this.uploads || []).filter(u => u.status === 'queued' || u.status === 'processing' || u.status === 'error');
        if (!ups.length) { host.innerHTML = ''; return; }
        // nach Kategorie gruppieren (feste Reihenfolge)
        const order = ['webseiten', 'dokumente', 'bilder', 'sprachnachrichten'];
        const groups = {};
        ups.forEach(u => { (groups[u.category] = groups[u.category] || []).push(u); });
        let html = '';
        order.filter(c => groups[c]).forEach(cat => {
            const meta = this._ingCat[cat] || { label: cat, icon: '' };
            html += `<div class="ju-group"><div class="ju-group-head">${meta.icon}<span>${meta.label}</span><span class="ju-count">${groups[cat].length}</span></div>`;
            groups[cat].forEach(u => { html += this._renderUploadItem(u); });
            html += `</div>`;
        });
        host.innerHTML = html;
    },
    _renderUploadItem(u) {
        const name = UI.escape(u.filename || u.source_url || 'Quelle');
        const busy = (u.status === 'queued' || u.status === 'processing');
        let status = '';
        if (busy) {
            const pct = Math.max(3, u.progress || 0);
            status = `<div class="ju-bar"><span style="width:${pct}%"></span></div>
                      <div class="ju-stage">${UI.escape(u.stage || 'In Arbeit')} · ${pct}%</div>`;
        } else if (u.status === 'error') {
            status = `<div class="ju-err">Fehler: ${UI.escape(u.error || 'unbekannt')}</div>`;
        } else if (u.status === 'done') {
            status = `<div class="ju-actions">${this._uploadActions(u)}</div>`;
        }
        return `<div class="ju-item ${u.status}" data-uid="${u.id}">
            <div class="ju-row">
                <span class="ju-name" title="${name}">${name}</span>
                <button class="ju-del" title="Entfernen" onclick="Studio.deleteUploadItem(${u.id})">&times;</button>
            </div>
            ${status}
            <div class="ju-media" id="ju-media-${u.id}"></div>
        </div>`;
    },
    _uploadActions(u) {
        const aid = u.article_id || 0;
        const btns = [];
        if (u.category === 'sprachnachrichten') {
            btns.push(`<button class="ju-btn" onclick="Studio.toggleText(${u.id},${aid})">Transkription</button>`);
            btns.push(`<button class="ju-btn" onclick="Studio.playAudio(${u.id})">Anhören</button>`);
        } else if (u.category === 'bilder') {
            btns.push(`<button class="ju-btn" onclick="Studio.toggleText(${u.id},${aid})">Text (OCR)</button>`);
            btns.push(`<button class="ju-btn" onclick="Studio.openOriginal(${u.id})">Bild öffnen</button>`);
        } else if (u.category === 'webseiten') {
            btns.push(`<button class="ju-btn" onclick="Studio.toggleText(${u.id},${aid})">Text ansehen</button>`);
            if (u.source_url) btns.push(`<a class="ju-btn" href="${UI.escape(u.source_url)}" target="_blank" rel="noopener">Link öffnen</a>`);
        } else {
            btns.push(`<button class="ju-btn" onclick="Studio.toggleText(${u.id},${aid})">Text ansehen</button>`);
            if (u.has_file) btns.push(`<button class="ju-btn" onclick="Studio.openOriginal(${u.id})">Original</button>`);
        }
        return btns.join('');
    },
    toggleText(uid, articleId) {
        const host = document.getElementById('ju-media-' + uid);
        if (!host) return;
        if (host.dataset.open === 'text') { host.innerHTML = ''; host.dataset.open = ''; return; }
        const art = (this.articles || []).find(a => a.id === articleId);
        const txt = art ? (art.content_de || art.content_original || '') : '';
        host.innerHTML = `<div class="ju-text">${txt ? UI.escape(txt).slice(0, 8000).replace(/\n/g, '<br>') : '<span class="empty-hint">Text wird vorbereitet …</span>'}</div>`;
        host.dataset.open = 'text';
        if (!art) this._refreshCorpus();
    },
    async playAudio(uid) {
        const host = document.getElementById('ju-media-' + uid);
        if (!host) return;
        if (host.dataset.open === 'audio') { host.innerHTML = ''; host.dataset.open = ''; return; }
        host.innerHTML = '<span class="empty-hint">Lädt …</span>';
        host.dataset.open = 'audio';
        try {
            const url = await API.fetchUploadBlobUrl(this.incident.id, uid);
            host.innerHTML = `<audio controls autoplay src="${url}" style="width:100%;margin-top:6px;"></audio>`;
        } catch (e) {
            host.innerHTML = `<span class="ju-err">${UI.escape(e.message || 'Audio-Fehler')}</span>`;
        }
    },
    async openOriginal(uid) {
        try {
            const url = await API.fetchUploadBlobUrl(this.incident.id, uid);
            window.open(url, '_blank', 'noopener');
        } catch (e) { UI.showToast(e.message || 'Datei-Fehler', 'error'); }
    },
    async deleteUploadItem(uid) {
        const ok = await this.askConfirm({
            title: 'Quelle entfernen?',
            okLabel: 'Entfernen',
            body: '<p class="confirm-warn">Die hochgeladene Datei und der daraus erzeugte Text werden entfernt.</p>',
        });
        if (!ok) return;
        try {
            await API.deleteUpload(this.incident.id, uid);
            this._seenDone.delete(uid);
            await this.loadUploads();
            this._refreshCorpus();
        } catch (e) { UI.showToast(e.message || 'Löschen fehlgeschlagen', 'error'); }
    },

    // === Spalte 3: Artefakte ===
    renderArtifacts(inc, fcs, locs, catLabels) {
        const sources = this._sources(inc);

        // Lagebild / Recherchebericht + Neueste Entwicklungen / Zusammenfassung
        // Bei Recherche die Zusammenfassung/Ueberblick aus dem Bericht herauslösen,
        // damit sie nicht doppelt (auch im eigenen Zusammenfassung-Tab) erscheint.
        const summaryEl = document.getElementById('art-summary-body');
        const latestEl = document.getElementById('art-latest-body');
        if (inc.type === 'research') {
            const ex = UI.extractZusammenfassung(inc.summary || '');
            const reportText = ex.zusammenfassung ? ex.remaining : (inc.summary || '');
            summaryEl.innerHTML = UI.renderSummary(reportText, sources, inc.type);
            latestEl.innerHTML = ex.zusammenfassung
                ? UI.renderZusammenfassung(ex.zusammenfassung, sources)
                : '<span class="empty-hint">Keine separate Zusammenfassung.</span>';
        } else {
            summaryEl.innerHTML = UI.renderSummary(inc.summary || '', sources, inc.type);
            latestEl.innerHTML = UI.renderLatestDevelopments(inc.latest_developments || '', sources);
        }

        // Faktencheck
        const fcBody = document.getElementById('art-fc-body');
        App._fcHidden = new Set();
        if (fcs && fcs.length) {
            document.getElementById('art-fc-meta').textContent = fcs.length + ' geprüft';
            const filters = UI.renderFactCheckFilters(fcs);
            fcBody.innerHTML = (filters ? `<div style="margin-bottom:8px;">${filters}</div>` : '')
                + fcs.map(fc => UI.renderFactCheck(fc)).join('');
        } else {
            document.getElementById('art-fc-meta').textContent = '';
            fcBody.innerHTML = '<span class="empty-hint">Noch keine Faktenchecks.</span>';
        }
        // Faktencheck-Historie (frühere, komplett-neu ersetzte Läufe)
        fcBody.insertAdjacentHTML('beforeend',
            `<details class="fc-history" ontoggle="Studio.onFcHistoryToggle(this)">
                <summary>Frühere Faktencheck-Läufe</summary>
                <div class="fc-history-body"><span class="empty-hint">Beim Öffnen geladen.</span></div>
            </details>`);

        // Karte (Kategorie-Legende kommt aus /locations mit)
        UI.renderMap(locs || [], catLabels || null);

        // Ereignis-Timeline: lazy beim Öffnen des Tabs laden
        this._tlLoaded = false;
        document.getElementById('art-tl-meta').textContent = '';
        document.getElementById('art-tl-body').innerHTML = '<span class="empty-hint">Wird beim Öffnen geladen.</span>';

        // Snapshots-Meta zurücksetzen
        document.getElementById('art-snap-meta').textContent = '';
        document.getElementById('art-snap-body').innerHTML = '<span class="empty-hint">Wird beim Öffnen geladen.</span>';
    },

    // Aktivitaets-/Ereignis-Timeline (lazy beim Öffnen des Tabs)
    async loadTimeline(force) {
        if (this._tlLoaded && !force) return;
        this._tlLoaded = true;
        const el = document.getElementById('art-tl-body');
        if (!this.incident) return;
        el.innerHTML = '<span class="empty-hint">Lade Ereignisse …</span>';
        let events = [];
        try {
            const res = await API.getEvents(this.incident.id);
            events = (res && res.events) || [];
        } catch (e) {
            el.innerHTML = '<span class="empty-hint">Ereignisse konnten nicht geladen werden.</span>';
            return;
        }
        document.getElementById('art-tl-meta').textContent = events.length + ' Ereignisse';
        if (!events.length) { el.innerHTML = '<span class="empty-hint">Noch keine Ereignisse.</span>'; return; }

        // nach Typ in Untersektionen gruppieren (Reihenfolge in der Gruppe: neueste zuerst)
        const groups = {};
        events.forEach(ev => { (groups[ev.type] = groups[ev.type] || []).push(ev); });
        const sections = this._evSections.slice();
        Object.keys(groups).forEach(k => {
            if (!sections.some(s => s.key === k)) {
                sections.push({ key: k, label: (this._evMeta[k] || {}).label || 'Sonstige' });
            }
        });

        el.innerHTML = sections
            .filter(s => groups[s.key] && groups[s.key].length)
            .map(s => {
                const icon = (this._evMeta[s.key] || {}).icon || '';
                const items = groups[s.key].map(ev => this._renderEvent(ev)).join('');
                return `<details class="ev-section sec-${s.key}" open>
                    <summary class="ev-section-head">
                        <span class="ev-section-icon">${icon}</span>
                        <span class="ev-section-title">${UI.escape(s.label)}</span>
                        <span class="ev-section-count">${groups[s.key].length}</span>
                    </summary>
                    <div class="ev-section-body">${items}</div>
                </details>`;
            }).join('');
    },

    _fmtTs(ts) {
        const d = parseUTC(ts);
        return d ? this._fmt(d, true) : '';
    },

    _renderEvent(ev) {
        const time = this._fmtTs(ev.ts);
        let body;
        if (ev.type === 'chat_qa') {
            const d = ev.detail || '';
            const marker = '— Antwort —';
            const mi = d.indexOf(marker);
            const q = (mi >= 0 ? d.slice(0, mi) : (ev.title || '')).trim();
            const a = mi >= 0 ? d.slice(mi + marker.length).trim() : '';
            body = `<div class="ev-title">${UI.escape(q)}</div>` +
                (a ? `<details class="ev-answer"><summary>Antwort anzeigen</summary><div class="ev-answer-body">${this._renderReply(a, [])}</div></details>` : '');
        } else if (ev.type === 'article_ingest') {
            const t = ev.url
                ? `<a href="${UI.escape(ev.url)}" target="_blank" rel="noopener">${UI.escape(ev.title || '')}</a>`
                : UI.escape(ev.title || '');
            body = `<div class="ev-title">${t}</div>` + (ev.source ? `<div class="ev-src">${UI.escape(ev.source)}</div>` : '');
        } else {
            body = `<div class="ev-title">${UI.escape(ev.title || '')}</div>` +
                (ev.source ? `<div class="ev-src">${UI.escape(ev.source)}</div>` : '');
        }
        return `<div class="ev-item"><span class="ev-time">${time}</span><div class="ev-main">${body}</div></div>`;
    },

    async loadSnapshots() {
        if (this._snapLoaded) return;
        this._snapLoaded = true;
        const body = document.getElementById('art-snap-body');
        try {
            const snaps = await API.getSnapshots(this.incident.id) || [];
            const list = Array.isArray(snaps) ? snaps : (snaps.snapshots || []);
            document.getElementById('art-snap-meta').textContent = list.length + ' Berichte';
            if (!list.length) { body.innerHTML = '<span class="empty-hint">Noch keine früheren Lageberichte.</span>'; return; }
            body.innerHTML = list.map(s => {
                const t = s.created_at ? this._fmt(parseUTC(s.created_at)) : '';
                const prev = UI.escape((s.summary_preview || '').trim());
                const meta = `${s.article_count || 0} Artikel · ${s.fact_check_count || 0} Faktenchecks`;
                return `<details class="hist-item" ontoggle="Studio.onSnapToggle(this, ${s.id})">
                    <summary><span class="hist-time">${t}</span><span class="hist-meta">${meta}</span></summary>
                    <div class="hist-body"><div class="hist-prev">${prev}${prev ? ' …' : ''}</div></div>
                </details>`;
            }).join('');
        } catch (e) {
            body.innerHTML = '<span class="empty-hint">Konnte Lageberichte nicht laden.</span>';
        }
    },
    async onSnapToggle(el, snapId) {
        if (!el.open || el.dataset.loaded) return;
        el.dataset.loaded = '1';
        const bodyEl = el.querySelector('.hist-body');
        bodyEl.innerHTML = '<span class="empty-hint">Lädt …</span>';
        try {
            const snap = await API.getSnapshot(this.incident.id, snapId);
            let sources = [];
            try { sources = JSON.parse(snap.sources_json || '[]'); } catch (_) {}
            bodyEl.innerHTML = UI.renderSummary(snap.summary || '', sources, this.incident && this.incident.type);
        } catch (e) {
            bodyEl.innerHTML = '<span class="empty-hint">Konnte Bericht nicht laden.</span>';
        }
    },
    async onFcHistoryToggle(el) {
        if (!el.open || el.dataset.loaded) return;
        el.dataset.loaded = '1';
        const body = el.querySelector('.fc-history-body');
        try {
            const res = await API.listFactcheckRuns(this.incident.id);
            const runs = (res && res.runs) || [];
            if (!runs.length) { body.innerHTML = '<span class="empty-hint">Noch keine früheren Läufe.</span>'; return; }
            body.innerHTML = runs.map(r => {
                const t = r.created_at ? this._fmt(parseUTC(r.created_at)) : '';
                return `<details class="hist-item" ontoggle="Studio.onFcRunToggle(this, ${r.id})">
                    <summary><span class="hist-time">${t}</span><span class="hist-meta">${r.fact_count || 0} Fakten</span></summary>
                    <div class="hist-body"><span class="empty-hint">Lädt …</span></div>
                </details>`;
            }).join('');
        } catch (e) {
            body.innerHTML = '<span class="empty-hint">Konnte Historie nicht laden.</span>';
        }
    },
    async onFcRunToggle(el, runId) {
        if (!el.open || el.dataset.loaded) return;
        el.dataset.loaded = '1';
        const body = el.querySelector('.hist-body');
        try {
            const res = await API.getFactcheckRun(this.incident.id, runId);
            const facts = (res && res.facts) || [];
            body.innerHTML = facts.length
                ? facts.map(fc => UI.renderFactCheck(fc)).join('')
                : '<span class="empty-hint">Keine Fakten in diesem Lauf.</span>';
        } catch (e) {
            body.innerHTML = '<span class="empty-hint">Konnte Lauf nicht laden.</span>';
        }
    },

    // === Verschiebbare Trennung Tab-Panel <-> Konversation ===
    _initDivider() {
        const divider = document.getElementById('center-divider');
        const center = document.getElementById('studio-center');
        if (!divider || !center) return;
        const chat = center.querySelector('.chat-region');
        // gespeicherte Höhe wiederherstellen
        const saved = parseInt(localStorage.getItem('studio_chat_h') || '', 10);
        if (saved) center.style.setProperty('--chat-h', saved + 'px');

        let startY = 0, startH = 0;
        const onMove = (e) => {
            const dy = e.clientY - startY;
            let h = startH - dy;                     // nach oben ziehen => Chat größer
            const minChat = 140;
            const maxChat = center.clientHeight - 180; // Tab-Panel behält Mindesthoehe
            h = Math.max(minChat, Math.min(Math.max(minChat, maxChat), h));
            center.style.setProperty('--chat-h', h + 'px');
        };
        const onUp = (e) => {
            divider.classList.remove('dragging');
            document.body.style.userSelect = '';
            document.removeEventListener('pointermove', onMove);
            document.removeEventListener('pointerup', onUp);
            try { divider.releasePointerCapture(e.pointerId); } catch (_) {}
            // Karte neu vermessen, falls Karten-Tab aktiv
            if (this.activeTab === 'map' && UI.invalidateMap) UI.invalidateMap();
            const cur = center.style.getPropertyValue('--chat-h');
            if (cur.endsWith('px')) localStorage.setItem('studio_chat_h', parseInt(cur, 10));
        };
        divider.addEventListener('pointerdown', (e) => {
            startY = e.clientY;
            startH = chat.getBoundingClientRect().height;
            divider.classList.add('dragging');
            document.body.style.userSelect = 'none';
            try { divider.setPointerCapture(e.pointerId); } catch (_) {}
            document.addEventListener('pointermove', onMove);
            document.addEventListener('pointerup', onUp);
            e.preventDefault();
        });
    },

    // Tabs per Drag&Drop umsortieren (Delegation auf der statischen Tabbar)
    _initTabDnD() {
        const bar = document.getElementById('center-tabbar');
        if (!bar) return;
        const clearMarkers = () => bar.querySelectorAll('.center-tab').forEach(t =>
            t.classList.remove('drop-before', 'drop-after'));

        bar.addEventListener('dragstart', (e) => {
            const tab = e.target.closest('.center-tab');
            if (!tab) return;
            this._dragKey = tab.dataset.art;
            e.dataTransfer.effectAllowed = 'move';
            try { e.dataTransfer.setData('text/plain', tab.dataset.art); } catch (_) {}
            tab.classList.add('dragging-tab');
        });
        bar.addEventListener('dragend', (e) => {
            const tab = e.target.closest('.center-tab');
            if (tab) tab.classList.remove('dragging-tab');
            this._dragKey = null;
            clearMarkers();
        });
        bar.addEventListener('dragover', (e) => {
            if (!this._dragKey) return;
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
            clearMarkers();
            const over = e.target.closest('.center-tab');
            if (over && over.dataset.art !== this._dragKey) {
                const r = over.getBoundingClientRect();
                const after = (e.clientX - r.left) > r.width / 2;
                over.classList.add(after ? 'drop-after' : 'drop-before');
            }
        });
        bar.addEventListener('drop', (e) => {
            if (!this._dragKey) return;
            e.preventDefault();
            const fromKey = this._dragKey;
            const over = e.target.closest('.center-tab');
            let toIndex;
            if (over && over.dataset.art !== fromKey) {
                const r = over.getBoundingClientRect();
                const after = (e.clientX - r.left) > r.width / 2;
                const overIdx = this.openTabs.indexOf(over.dataset.art);
                toIndex = after ? overIdx + 1 : overIdx;
            } else {
                toIndex = this.openTabs.length;
            }
            const fromIdx = this.openTabs.indexOf(fromKey);
            if (fromIdx === -1) return;
            this.openTabs.splice(fromIdx, 1);
            if (fromIdx < toIndex) toIndex--;
            this.openTabs.splice(toIndex, 0, fromKey);
            this._dragKey = null;
            clearMarkers();
            this._renderTabbar();   // baut Tabs neu, aktiver Zustand bleibt erhalten
        });
    },

    // === Studio-Tabs in der Mitte ===
    // === Einstellungen des Falls =========================================
    // Der Reiter ersetzt das Bearbeiten-Modal der klassischen Ansicht. Er
    // schickt beim Speichern nur, was sich wirklich geaendert hat. Zwei Gruende.
    // Der Server aktualisiert feldweise, alles Mitgeschickte wuerde sonst
    // ueberschrieben, auch wenn es nur unveraendert zurueckgereicht wird. Und
    // eine unbeabsichtigte Aenderung am Intervall kann einen laufenden Zeitplan
    // verstellen.

    _setEl(id) { return document.getElementById(id); },

    /** Liest das Formular. Der Rueckgabewert ist mit dem Schnappschuss vergleichbar. */
    _settingsWerte() {
        const g = (id) => this._setEl(id);
        const einheit = parseInt(g('set-refresh-unit').value) || 1;
        const zahl = parseInt(g('set-refresh-value').value) || 15;
        const auto = g('set-refresh-mode').value === 'auto';
        const werte = {
            title: g('set-title').value.trim(),
            description: g('set-description').value.trim(),
            international_sources: g('set-international').checked,
            include_telegram: g('set-telegram').checked,
            include_x: g('set-x').checked,
            visibility: g('set-visibility').checked ? 'public' : 'private',
            ai_backend: g('set-ai-backend').value || '',
            refresh_mode: g('set-refresh-mode').value,
            refresh_interval: Math.max(10, Math.min(10080, zahl * einheit)),
            retention_days: Math.max(0, Math.min(999, parseInt(g('set-retention').value) || 0)),
        };
        // Die Startzeit gehoert nur zur automatischen Aktualisierung. Bei
        // manueller Aktualisierung bleibt der gespeicherte Wert unangetastet,
        // ausgewertet wird er dort ohnehin nicht.
        if (auto) werte.refresh_start_time = g('set-refresh-starttime').value || '07:00';
        return werte;
    },

    _settingsAbo() {
        return {
            notify_email_summary: this._setEl('set-notify-summary').checked,
            notify_email_new_articles: this._setEl('set-notify-new-articles').checked,
            notify_email_status_change: this._setEl('set-notify-status-change').checked,
        };
    },

    /** Fuellt das Formular aus dem offenen Fall. Dient auch als Verwerfen. */
    async loadSettings() {
        const inc = this.incident;
        if (!inc || !this._setEl('set-title')) return;
        const g = (id) => this._setEl(id);

        g('set-title').value = inc.title || '';
        g('set-description').value = inc.description || '';
        g('set-international').checked = !!inc.international_sources;
        g('set-telegram').checked = !!inc.include_telegram;
        g('set-x').checked = !!inc.include_x;
        g('set-visibility').checked = (inc.visibility || 'public') === 'public';
        g('set-ai-backend').value = inc.ai_backend || '';
        g('set-refresh-mode').value = inc.refresh_mode === 'auto' ? 'auto' : 'manual';
        g('set-refresh-starttime').value = inc.refresh_start_time || '07:00';
        g('set-retention').value = (inc.retention_days === null || inc.retention_days === undefined)
            ? 0 : inc.retention_days;

        // Intervall in die groesste Einheit zeigen, die glatt aufgeht. 1440
        // Minuten liest sich als 1 Tag besser denn als 1440 Minuten.
        const iv = parseInt(inc.refresh_interval) || 15;
        let einheit = 1;
        for (const u of [10080, 1440, 60]) { if (iv % u === 0) { einheit = u; break; } }
        g('set-refresh-unit').value = String(einheit);
        g('set-refresh-value').value = String(Math.round(iv / einheit));

        this.settingsRefreshToggle();
        this.settingsIntervalMin();
        this.settingsVisibilityHint();
        this._settingsRunningNote();

        // Der Schnappschuss ist die Vergleichsgrundlage fuers Speichern. Er
        // wird vor dem Abo-Nachladen gesetzt, damit ein langsamer Abruf das
        // Formular nicht in einem halben Zustand zuruecklaesst.
        this._settingsAlt = this._settingsWerte();
        this.settingsAiChanged();
        this._settingsState('');

        try {
            const abo = await API.getSubscription(inc.id);
            g('set-notify-summary').checked = !!(abo && abo.notify_email_summary);
            g('set-notify-new-articles').checked = !!(abo && abo.notify_email_new_articles);
            g('set-notify-status-change').checked = !!(abo && abo.notify_email_status_change);
        } catch (e) {
            // Ohne Abo-Stand bleiben die Haken aus. Das ist der Serverzustand
            // fuer einen Fall ohne Abo.
            g('set-notify-summary').checked = false;
            g('set-notify-new-articles').checked = false;
            g('set-notify-status-change').checked = false;
        }
        this._settingsAboAlt = this._settingsAbo();

        // Bewusst abgewartet. Erst danach ist das Formular vollstaendig,
        // vorher stuende der X-Schalter noch auf dem Ausgangswert.
        await this._settingsXHint();
    },

    settingsRefreshToggle() {
        const auto = this._setEl('set-refresh-mode').value === 'auto';
        this._setEl('set-interval-field').classList.toggle('visible', auto);
        this._setEl('set-starttime-field').classList.toggle('visible', auto);
    },

    settingsIntervalMin() {
        const einheit = parseInt(this._setEl('set-refresh-unit').value);
        const feld = this._setEl('set-refresh-value');
        const min = einheit === 1 ? 10 : 1;
        feld.min = min;
        if (parseInt(feld.value) < min) feld.value = min;
    },

    settingsVisibilityHint() {
        const oeffentlich = this._setEl('set-visibility').checked;
        this._setEl('set-visibility-text').textContent = oeffentlich
            ? 'Öffentlich, für alle Nutzer sichtbar'
            : 'Privat, nur für dich sichtbar';
    },

    /** Warnt, sobald der KI-Weg vom gespeicherten Stand abweicht. */
    settingsAiChanged() {
        const w = this._setEl('set-ai-warn');
        if (!w) return;
        const jetzt = this._setEl('set-ai-backend').value || '';
        const alt = (this._settingsAlt && this._settingsAlt.ai_backend) || '';
        w.hidden = jetzt === alt;
    },

    _settingsRunningNote() {
        const n = this._setEl('set-running-note');
        if (n) n.hidden = !this._runningStage;
    },

    _settingsState(text, art) {
        const s = this._setEl('set-state');
        if (!s) return;
        s.textContent = text || '';
        s.className = 'settings-state' + (art ? ' ' + art : '');
    },

    /** X laesst sich nur einschalten, wenn ein Zugang hinterlegt ist. */
    async _settingsXHint() {
        const cb = this._setEl('set-x');
        const hint = this._setEl('set-x-hint');
        if (!cb) return;
        let aktiv = 0;
        try {
            const konten = await API.listXAccounts();
            aktiv = (konten || []).filter(a => a.active).length;
        } catch (e) {
            aktiv = 0;   // im Zweifel sperren
        }
        // Nutzt der Fall X bereits, bleibt der Schalter bedienbar. Sonst koennte
        // man ihn nach dem Wegfall des letzten Zugangs nicht mehr abschalten.
        cb.disabled = aktiv === 0 && !cb.checked;
        if (hint) hint.hidden = aktiv !== 0;
    },

    async saveSettings() {
        const inc = this.incident;
        if (!inc || !this._setEl('set-title')) return;
        const knopf = this._setEl('set-save');

        const jetzt = this._settingsWerte();
        if (!jetzt.title) {
            UI.showToast('Ohne Titel geht es nicht', 'error');
            this._setEl('set-title').focus();
            return;
        }

        const alt = this._settingsAlt || {};
        const patch = {};
        Object.keys(jetzt).forEach(k => {
            if (jetzt[k] !== alt[k]) patch[k] = jetzt[k];
        });

        const abo = this._settingsAbo();
        const aboAlt = this._settingsAboAlt || {};
        const aboGeaendert = Object.keys(abo).some(k => abo[k] !== aboAlt[k]);

        if (!Object.keys(patch).length && !aboGeaendert) {
            this._settingsState('Nichts geändert');
            return;
        }

        knopf.disabled = true;
        this._settingsState('Wird gespeichert …');
        try {
            if (Object.keys(patch).length) {
                const neu = await API.updateIncident(inc.id, patch);
                // Der Server antwortet mit der angereicherten Lage. Sie ist die
                // verlaessliche Quelle, das Formular koennte abweichen.
                this.incident = (neu && neu.id) ? neu : Object.assign({}, inc, patch);
                const i = this.incidents.findIndex(x => x.id === inc.id);
                if (i >= 0) this.incidents[i] = Object.assign({}, this.incidents[i], this.incident);
            }
            if (aboGeaendert) await API.updateSubscription(inc.id, abo);

            this._settingsAlt = this._settingsWerte();
            this._settingsAboAlt = abo;
            this.settingsAiChanged();
            this.renderHeader(this.incident);
            this.renderCases();
            this._settingsState('Gespeichert', 'ok');
            UI.showToast('Einstellungen gespeichert', 'success');
        } catch (e) {
            this._settingsState('Nicht gespeichert', 'fehler');
            UI.showToast((e && e.message) || 'Einstellungen konnten nicht gespeichert werden', 'error');
        } finally {
            knopf.disabled = false;
        }
    },

    /** Kennzeichen des KI-Wegs. Leer, solange die Org-Vorgabe gilt. */
    _aiTag(inc, klasse) {
        const b = inc && inc.ai_backend;
        if (b === 'bedrock') {
            return `<span class="${klasse} ai-eu" title="KI-Verarbeitung in der EU (AWS Bedrock, Frankfurt), Recherche über den europäischen Suchindex staan">EU</span>`;
        }
        if (b === 'cli') {
            return `<span class="${klasse} ai-anthropic" title="KI-Verarbeitung über Anthropic mit eingebauter Websuche (heutiger Weg)">Anthropic</span>`;
        }
        return '';
    },

    _tabTitle(key) {
        const el = document.querySelector(`#studio-menu .studio-menu-item[data-art="${CSS.escape(key)}"] .mi-title`);
        return el ? el.textContent.trim() : key;
    },
    _tabIcon(key) {
        const el = document.querySelector(`#studio-menu .studio-menu-item[data-art="${CSS.escape(key)}"] .mi-icon`);
        return el ? el.innerHTML : '';
    },

    _initTabs() {
        this.openTabs = [];
        this.activeTab = null;
        document.getElementById('studio-center').classList.remove('has-tabs');
        document.getElementById('center-tabbar').innerHTML = '';
        document.querySelectorAll('#center-tab-content .tab-panel').forEach(p => p.classList.remove('active'));
        document.querySelectorAll('#studio-menu .studio-menu-item').forEach(m => m.classList.remove('open', 'active'));
        // Standard: Lagebild/Recherchebericht als erster Tab
        this.openTab('summary');
    },

    _renderTabbar() {
        const bar = document.getElementById('center-tabbar');
        bar.innerHTML = this.openTabs.map(key => {
            const k = key.replace(/'/g, "\\'");
            return `<div class="center-tab${key === this.activeTab ? ' active' : ''}" data-art="${key}" draggable="true" onclick="Studio.activateTab('${k}')" role="tab">
                <span class="ct-icon">${this._tabIcon(key)}</span>
                <span class="ct-title">${UI.escape(this._tabTitle(key))}</span>
                <span class="center-tab-close" title="Schließen" onclick="Studio.closeTab('${k}', event)">${this._closeSvg}</span>
            </div>`;
        }).join('');
        document.querySelectorAll('#studio-menu .studio-menu-item').forEach(m => {
            m.classList.toggle('open', this.openTabs.includes(m.dataset.art));
        });
    },

    openTab(key) {
        if (!this.openTabs.includes(key)) this.openTabs.push(key);
        document.getElementById('studio-center').classList.add('has-tabs');
        this._renderTabbar();
        this.activateTab(key);
    },

    activateTab(key) {
        this.activeTab = key;
        document.querySelectorAll('#center-tab-content .tab-panel').forEach(p => {
            p.classList.toggle('active', p.dataset.art === key);
        });
        document.querySelectorAll('#center-tabbar .center-tab').forEach(t => {
            t.classList.toggle('active', t.dataset.art === key);
        });
        document.querySelectorAll('#studio-menu .studio-menu-item').forEach(m => {
            m.classList.toggle('active', m.dataset.art === key);
        });
        // Nebenwirkungen: Karte neu vermessen / Snapshots lazy laden
        if (key === 'map') {
            setTimeout(() => {
                if (UI.retryPendingMap) UI.retryPendingMap();
                if (UI.invalidateMap) UI.invalidateMap();
            }, 60);
        }
        if (key === 'snapshots') this.loadSnapshots();
        if (key === 'timeline') this.loadTimeline();
        if (key === 'settings') this.loadSettings();
    },

    closeTab(key, ev) {
        if (ev) ev.stopPropagation();
        const idx = this.openTabs.indexOf(key);
        if (idx === -1) return;
        this.openTabs.splice(idx, 1);
        const mi = document.querySelector(`#studio-menu .studio-menu-item[data-art="${CSS.escape(key)}"]`);
        if (mi) mi.classList.remove('open', 'active');

        if (!this.openTabs.length) {
            this.activeTab = null;
            document.getElementById('studio-center').classList.remove('has-tabs');
            this._renderTabbar();
            document.querySelectorAll('#center-tab-content .tab-panel').forEach(p => p.classList.remove('active'));
            return;
        }
        // Nachbar-Tab aktivieren, wenn der aktive geschlossen wurde
        let nextKey = this.activeTab;
        if (this.activeTab === key || !this.openTabs.includes(this.activeTab)) {
            nextKey = this.openTabs[Math.min(idx, this.openTabs.length - 1)];
        }
        this._renderTabbar();
        this.activateTab(nextKey);
    },

    // === Spalte 2: RAG-Chat ===
    renderChatIntro(inc) {
        const box = document.getElementById('chat-messages');
        const sources = this._sources(inc);
        let intro = '';
        if (inc.summary) {
            const ex = inc.type === 'research' ? UI.extractZusammenfassung(inc.summary) : { zusammenfassung: null };
            const short = ex.zusammenfassung
                ? UI.renderZusammenfassung(ex.zusammenfassung, sources)
                : UI.renderSummary(inc.summary, sources, inc.type);
            intro = `<div class="chat-msg chat-msg-assistant pinned">
                <div class="chat-pin-label">${inc.type === 'research' ? 'Recherchebericht' : 'Lagebild'}</div>
                ${short}</div>`;
        } else {
            intro = `<div class="chat-msg chat-msg-assistant">Zu diesem Fall gibt es noch kein Lagebild. Starte eine Aktualisierung.</div>`;
        }
        box.innerHTML = intro;
        box.scrollTop = 0;

        // Bewusst keine Vorschlagsfragen mehr (auch nicht bei "Neu"): der Chat
        // startet leer; gestellte Fragen landen in der Ereignis-Timeline.
        document.getElementById('chat-suggestions').innerHTML = '';
    },

    askChip(btn) {
        document.getElementById('chat-input').value = btn.textContent;
        this.sendChat();
    },
    toggleScope(on) {
        this.searchAllScope = !!on;
        const lbl = document.getElementById('chat-scope');
        if (lbl) lbl.classList.toggle('on', this.searchAllScope);
    },
    autoGrow(ta) { ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, 120) + 'px'; },
    chatKey(e) {
        if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendChat(); }
    },
    resetChat() {
        this.convId = null;
        if (this.incident) this.renderChatIntro(this.incident);
    },

    async sendChat() {
        const ta = document.getElementById('chat-input');
        const msg = (ta.value || '').trim();
        if (!msg || !this.incident) return;
        const box = document.getElementById('chat-messages');
        box.insertAdjacentHTML('beforeend',
            `<div class="chat-msg chat-msg-user">${UI.escape(msg)}</div>`);
        ta.value = ''; this.autoGrow(ta);
        const thinking = document.createElement('div');
        thinking.className = 'chat-msg chat-msg-assistant chat-thinking';
        thinking.textContent = 'Denkt nach ...';
        box.appendChild(thinking);
        box.scrollTop = box.scrollHeight;

        document.getElementById('chat-send').disabled = true;
        try {
            const res = await API.askIncident(this.incident.id, msg, {
                conversation_id: this.convId,
                source_filter: this._activeSourceFilter(),
                scope: this.searchAllScope ? 'alle' : 'fall',
            });
            this.convId = res.conversation_id || this.convId;
            thinking.classList.remove('chat-thinking');
            thinking.innerHTML = this._renderReply(res.reply || '', res.sources || []);
            // Analyst bietet gezielte Recherche an, wenn die Materialien nicht reichen
            if (res.offer && res.offer.needs_research) {
                thinking.insertAdjacentHTML('beforeend', this._renderOffer(res.offer));
            }
            // Frage & Antwort wurden serverseitig protokolliert -> Timeline auffrischen
            this._tlLoaded = false;
            if (this.activeTab === 'timeline') this.loadTimeline(true);
        } catch (e) {
            thinking.classList.remove('chat-thinking');
            const notReady = e && (e.status === 404 || e.status === 405);
            thinking.innerHTML = notReady
                ? '<span class="empty-hint">Der Lage-Chat ist noch nicht aktiviert (Backend-Neustart erforderlich).</span>'
                : '<span class="empty-hint">' + UI.escape((e && e.message) || 'Fehler bei der Anfrage.') + '</span>';
        } finally {
            document.getElementById('chat-send').disabled = false;
            box.scrollTop = box.scrollHeight;
        }
    },

    _renderReply(text, sources) {
        // Zitate [n] klickbar machen -> highlightet Quelle in Spalte 1
        let html = UI.escape(text);
        html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
        html = html.replace(/\n/g, '<br>');
        html = html.replace(/\[(\d+)\]/g, (m, n) => {
            const src = (sources || []).find(s => String(s.nr) === n);
            const title = src ? UI.escape(src.source || src.name || '') : '';
            return `<span class="chat-cite" title="${title}" onclick="Studio.gotoCite('${src ? UI.escape(src.source || src.name || '') : ''}')">[${n}]</span>`;
        });
        return html;
    },

    gotoCite(sourceName) {
        if (!sourceName) return;
        const item = document.querySelector(`.src-item[data-source="${CSS.escape(sourceName)}"]`);
        if (!item) { UI.showToast('Quelle nicht in der Liste', 'info'); return; }
        item.classList.add('open');
        item.scrollIntoView({ behavior: 'smooth', block: 'center' });
        item.classList.remove('cite-flash'); void item.offsetWidth;
        item.classList.add('cite-flash');
    },

    // Recherche-Angebot des Analysten (wenn die Materialien die Frage nicht hergeben)
    _renderOffer(offer) {
        const focus = UI.escape(offer.focus || '');
        const add = UI.escape(offer.description_addition || '');
        const payload = encodeURIComponent(JSON.stringify({
            focus: offer.focus || '', description_addition: offer.description_addition || '',
        }));
        return `<div class="chat-offer" data-offer="${payload}">
            <div class="chat-offer-title">Dazu liegt in diesem Fall noch nichts vor.</div>
            <div class="chat-offer-text">Ich kann die Fallbeschreibung um diesen Aspekt ergänzen und gezielt dazu recherchieren, ausschließlich zu dieser Frage.</div>
            <label class="chat-offer-label">Ergänzung der Fallbeschreibung (editierbar):</label>
            <textarea class="chat-offer-add" rows="2">${add}</textarea>
            <div class="chat-offer-focus">Suchfokus: <em>${focus}</em></div>
            <button class="studio-btn chat-offer-btn" type="button" onclick="Studio.runClarify(this)">Ergänzen und gezielt recherchieren</button>
        </div>`;
    },

    async runClarify(btn) {
        const card = btn.closest('.chat-offer');
        if (!card || !this.incident) return;
        let data = {};
        try { data = JSON.parse(decodeURIComponent(card.dataset.offer || '%7B%7D')); } catch (e) {}
        const addEl = card.querySelector('.chat-offer-add');
        const description_addition = addEl ? (addEl.value || '').trim() : (data.description_addition || '');
        const focus = data.focus || '';
        if (!focus) return;
        btn.disabled = true; btn.textContent = 'Recherche läuft …';
        try {
            await API.clarify(this.incident.id, { focus, description_addition });
            this._pollClarify(card);
        } catch (e) {
            btn.disabled = false; btn.textContent = 'Ergänzen und gezielt recherchieren';
            UI.showToast((e && e.message) || 'Recherche konnte nicht gestartet werden', 'error');
        }
    },

    _pollClarify(card) {
        const incId = this.incident && this.incident.id;
        if (!incId) return;
        const btn = card.querySelector('.chat-offer-btn');
        const textEl = card.querySelector('.chat-offer-text');
        const tick = async () => {
            let st = null;
            try { const r = await API.getRunStatus(incId); st = r && r.state; } catch (e) {}
            if (st && st.status === 'running') { setTimeout(tick, 4000); return; }
            if (st && st.status === 'done') {
                const n = (st.result && st.result.inserted) || 0;
                card.innerHTML = `<div class="chat-offer-title">Gezielte Recherche abgeschlossen.</div>
                    <div class="chat-offer-text">${n} neue Meldung${n === 1 ? '' : 'en'} zur Frage erfasst. Starte Analyse oder Faktencheck neu und stelle die Frage erneut.</div>`;
                this._tlLoaded = false;
                try { this.fresh = await API.getFreshness(incId); this._renderLauf(); } catch (e) {}
            } else if (st && st.status === 'error') {
                if (btn) { btn.disabled = false; btn.textContent = 'Erneut versuchen'; }
                if (textEl) textEl.textContent = 'Die Recherche ist fehlgeschlagen. Bitte erneut versuchen.';
            } else {
                setTimeout(tick, 4000);
            }
        };
        setTimeout(tick, 3000);
    },

    // === Der Lauf (Spalte 3) =============================================
    // Es gibt genau einen Startpunkt. Die vier Schritte darunter sind Anzeige,
    // sie starten nichts. Aufklappen erklaert nur, was ein Schritt tut.
    //
    // Der Server kann einzelne Bausteine weiterhin starten (POST .../stage).
    // Die Oberflaeche nutzt das nicht mehr, zeigt einen so gestarteten Lauf
    // aber korrekt an, denn er kann aus der klassischen Ansicht kommen.
    _CANCELLABLE: ['collect', 'full'],
    // 'network' hat keine Karte mehr, das Label bleibt trotzdem stehen. Kaeme
    // der Baustein im Server, meldete ein laufender Schritt sonst nur seinen
    // technischen Schluessel.
    _STAGE_LABELS: {
        collect: 'Artikel holen', analyze: 'Lagebild schreiben',
        factcheck: 'Fakten prüfen', geoparse: 'Orte erkennen',
        network: 'Netzwerkanalyse', full: 'Kompletter Lauf',
    },

    _SCHRITT_ICONS: {
        collect: '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
        analyze: '<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>',
        factcheck: '<path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/>',
        geoparse: '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/>',
    },

    _SCHRITT_WAS: {
        collect: 'Durchsucht die ausgewählten Quellen nach neuen Meldungen. Ohne diesen Schritt haben die folgenden nichts, womit sie arbeiten können.',
        analyze: 'Wertet alle Artikel aus und schreibt daraus den zusammenhängenden Bericht. Der bisherige Bericht bleibt im Verlauf erhalten.',
        factcheck: 'Zieht die Tatsachenbehauptungen aus dem Bericht und sucht nach Belegen und Widersprüchen. Der bisherige Stand wird als Lauf archiviert.',
        geoparse: 'Findet Ortsangaben in den Artikeln und setzt sie auf die Karte. Läuft nur über Artikel, die noch nicht geprüft wurden.',
    },

    _schrittOffen: null,

    /** Von aussen (Tastatur, WebSocket-Resync) heisst Aktualisieren kompletter Lauf. */
    async refresh() { return this.runStage('full'); },

    /**
     * Der eine Startknopf. Laeuft gerade ein abbrechbarer Schritt, bricht er ab,
     * sonst startet er den kompletten Lauf.
     *
     * Wichtig ist der laufende Schritt, nicht 'full'. Ein Lauf kann aus der
     * klassischen Ansicht als 'collect' gestartet worden sein. Ein Abbruch auf
     * 'full' wuerde ihn dann nicht treffen, obwohl der Knopf Abbrechen anzeigt.
     */
    async laufKnopf() {
        const run = this._runningStage;
        if (run && this._CANCELLABLE.includes(run)) return this.runStage(run);
        if (run) {
            UI.showToast('Dieser Schritt lässt sich nicht abbrechen.', 'info');
            return;
        }
        return this.runStage('full');
    },

    _stageEls() { return Array.from(document.querySelectorAll('[data-stage]')); },
    _stageEl(stage) { return document.querySelector(`[data-stage="${CSS.escape(stage)}"]`); },

    _stagesIdle() {
        this._runningStage = null;
        this._stageEls().forEach(b => {
            b.classList.remove('running');
            b.disabled = !this.incident;
        });
        this._renderLauf();
    },

    _stageRunning(stage, text) {
        if (!stage) return;
        this._runningStage = stage;
        // Sobald etwas läuft, ist die Startfrage beantwortet.
        const panel = document.getElementById('start-panel');
        if (panel) panel.hidden = true;
        if (this.incident) localStorage.setItem('studio_start_' + this.incident.id, 'run');
        const cancellable = this._CANCELLABLE.includes(stage);
        this._stageEls().forEach(b => {
            b.classList.add('running');
            // Der Startknopf bleibt nur klickbar, wenn sich der Lauf abbrechen lässt
            b.disabled = !cancellable;
        });
        this._renderLauf(text);
    },

    klappSchritt(i) {
        this._schrittOffen = (this._schrittOffen === i) ? null : i;
        this._renderLauf();
    },

    // App-Zeitstempel stehen bereits in lokaler Zeit -> nur umformatieren, NICHT als UTC lesen
    _fmtStamp(ts) {
        const m = String(ts || '').match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})/);
        return m ? `${m[3]}.${m[2]}. ${m[4]}:${m[5]}` : '';
    },

    _svg(pfad, groesse) {
        return `<svg xmlns="http://www.w3.org/2000/svg" width="${groesse}" height="${groesse}" `
             + 'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '
             + `stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${pfad}</svg>`;
    },

    /**
     * Zustand der vier Schritte aus /freshness ableiten.
     * 'fertig' | 'veraltet' | 'offen' | 'gesperrt' | 'laeuft' | 'wartet'
     */
    _schrittZustaende() {
        const f = this.fresh || {};
        const run = this._runningStage;
        const arts = f.articles || 0;
        const s = f.summary || {};
        const fc = f.factcheck || {};
        const geo = f.geoparse || {};

        const zust = (key, ohneLauf) => {
            if (run === key) return 'laeuft';
            // Der komplette Lauf und das Sammeln arbeiten alle Schritte ab
            if (run === 'full' || run === 'collect') return 'wartet';
            if (run) return 'wartet';
            return ohneLauf();
        };

        return [
            {
                key: 'collect',
                zustand: zust('collect', () => (arts ? 'fertig' : 'offen')),
                stand: run === 'collect' ? 'läuft gerade'
                     : arts ? `${arts} Artikel${f.last_collect ? ', ' + this._fmtStamp(f.last_collect) : ''}`
                     : 'noch nicht gelaufen',
            },
            {
                key: 'analyze',
                zustand: zust('analyze', () => !arts ? 'gesperrt'
                                                : !s.exists ? 'offen'
                                                : s.pending > 0 ? 'veraltet' : 'fertig'),
                stand: run === 'analyze' ? 'läuft gerade'
                     : !arts ? 'wartet auf Artikel'
                     : !s.exists ? 'noch nicht erzeugt'
                     : s.pending > 0 ? `${s.pending} neue Artikel seit dem Bericht`
                     : `aktuell, ${this._fmtStamp(s.last)}`,
            },
            {
                key: 'factcheck',
                zustand: zust('factcheck', () => !arts ? 'gesperrt'
                                                  : !fc.exists ? 'offen'
                                                  : fc.pending > 0 ? 'veraltet' : 'fertig'),
                stand: run === 'factcheck' ? 'läuft gerade'
                     : !arts ? 'wartet auf Artikel'
                     : !fc.exists ? 'noch nicht erzeugt'
                     : fc.pending > 0 ? `${fc.pending} neue Artikel seit der Prüfung`
                     : `${fc.facts} Fakten geprüft`,
            },
            {
                key: 'geoparse',
                zustand: zust('geoparse', () => !arts ? 'gesperrt'
                                                 : geo.pending > 0 ? 'veraltet' : 'fertig'),
                stand: run === 'geoparse' ? 'läuft gerade'
                     : !arts ? 'wartet auf Artikel'
                     : geo.pending > 0 ? `${geo.pending} Artikel noch nicht verortet`
                     : 'alle Artikel verortet',
            },
        ];
    },

    /** Stand-Kasten, Kette und Schrittliste zeichnen. */
    _renderLauf(runText) {
        const kasten = document.getElementById('lauf-stand');
        if (!kasten) return;
        const inc = this.incident;
        const f = this.fresh || {};
        const run = this._runningStage;
        const arts = f.articles || 0;
        const research = inc && inc.type === 'research';

        const haupt = document.getElementById('ls-haupt');
        const neben = document.getElementById('ls-neben');
        const knopf = document.getElementById('ls-btn');
        const text = knopf && knopf.querySelector('.ls-text');
        const iconEl = knopf && knopf.querySelector('.ls-icon');
        const hinweis = document.getElementById('ls-hinweis');

        const ICON_LAUF = '<path d="M3 12a9 9 0 0 1 15.5-6.4L21 8"/><polyline points="21 3 21 8 16 8"/><path d="M21 12a9 9 0 0 1-15.5 6.4L3 16"/><polyline points="3 21 3 16 8 16"/>';
        const ICON_STOPP = '<rect width="12" height="12" x="6" y="6" rx="1"/>';

        // --- Stand-Kasten ---
        let ueberschrift = '';
        let knopfText = 'Aktualisieren';
        let abbruch = false;
        let betont = false;

        if (!inc) {
            ueberschrift = 'Kein Fall geöffnet';
        } else if (run) {
            ueberschrift = (this._STAGE_LABELS[run] || 'Ein Schritt') + ' läuft gerade';
            abbruch = this._CANCELLABLE.includes(run);
            knopfText = abbruch ? 'Abbrechen' : (runText || 'läuft …');
        } else if (!arts) {
            ueberschrift = 'Noch keine Artikel gesammelt';
            knopfText = research ? 'Recherche starten' : 'Lauf starten';
            betont = true;
        } else {
            const offen = this._schrittZustaende()
                .filter(s => s.zustand === 'veraltet' || s.zustand === 'offen');
            if (offen.length) {
                const s = f.summary || {};
                ueberschrift = s.pending > 0
                    ? `${s.pending} neue Artikel seit dem letzten Bericht`
                    : 'Ergebnisse sind nicht auf dem neuesten Stand';
                knopfText = 'Jetzt aktualisieren';
                betont = true;
            } else {
                ueberschrift = 'Alle Ergebnisse auf dem neuesten Stand';
                knopfText = 'Erneut durchlaufen';
            }
        }

        kasten.classList.toggle('betont', betont);
        if (haupt) {
            haupt.textContent = ueberschrift;
            haupt.classList.toggle('warn', betont && !!arts);
        }
        if (neben) {
            neben.textContent = !inc ? ''
                : arts ? `${arts} Artikel im Bestand`
                : 'Noch kein Bestand';
        }
        if (text) text.textContent = knopfText;
        if (iconEl) iconEl.innerHTML = this._svg(abbruch ? ICON_STOPP : ICON_LAUF, 16);
        if (knopf) {
            knopf.classList.toggle('abbrechen', abbruch);
            knopf.disabled = !inc || (!!run && !abbruch);
            knopf.title = abbruch ? 'Laufenden Schritt abbrechen'
                : research ? 'Alle Schritte nacheinander, am Ende steht ein fertiger Recherchebericht'
                : 'Alle Schritte nacheinander, am Ende steht ein fertiges Lagebild';
        }
        if (hinweis) {
            hinweis.textContent = (!inc || run || arts) ? ''
                : 'Danach entstehen Bericht, Faktencheck und Karte von selbst.';
            hinweis.hidden = !hinweis.textContent;
        }

        // --- Kette und Schritte ---
        const schritte = inc ? this._schrittZustaende() : [];
        const kette = document.getElementById('lauf-kette');
        const fortschritt = document.getElementById('lauf-fortschritt');
        const liste = document.getElementById('lauf-schritte');

        if (kette) {
            kette.innerHTML = schritte.map((s, i) => {
                const kn = `<span class="kn kn-${s.zustand}"></span>`;
                if (i === schritte.length - 1) return kn;
                const voll = s.zustand === 'fertig' && schritte[i + 1].zustand === 'fertig';
                return kn + `<span class="kn-linie${voll ? ' aktiv' : ''}"></span>`;
            }).join('');
        }
        if (fortschritt) {
            const fertig = schritte.filter(s => s.zustand === 'fertig').length;
            fortschritt.textContent = inc
                ? `${fertig} von ${schritte.length} Schritten auf dem neuesten Stand` : '';
        }

        if (liste) {
            liste.innerHTML = schritte.map((s, i) => {
                const auf = this._schrittOffen === i;
                const name = (s.key === 'analyze' && research)
                    ? 'Recherchebericht schreiben' : this._STAGE_LABELS[s.key];
                const marke = s.zustand === 'fertig'
                    ? this._svg('<path d="M20 6 9 17l-5-5"/>', 12) : String(i + 1);
                return `<div class="schritt schritt-${s.zustand}${auf ? ' offen' : ''}">
                    <button class="schritt-kopf" type="button" onclick="Studio.klappSchritt(${i})"
                            aria-expanded="${auf}">
                        <span class="schritt-nr">${marke}</span>
                        <span class="schritt-txt">
                            <span class="schritt-name">${UI.escape(name)}</span>
                            <span class="schritt-stand">${UI.escape(s.stand)}</span>
                        </span>
                        <span class="schritt-pfeil">${this._svg('<polyline points="6 9 12 15 18 9"/>', 13)}</span>
                    </button>
                    <div class="schritt-mehr">${UI.escape(this._SCHRITT_WAS[s.key] || '')}</div>
                </div>`;
            }).join('');
        }

        // --- Nebenangaben an den Ansichten ---
        this._renderMeta();
    },

    /** Zusatzangaben an den Ansichtsknoepfen, aus /freshness. */
    _renderMeta() {
        const f = this.fresh;
        if (!f) return;
        const arts = f.articles || 0;
        const s = f.summary || {};
        const fc = f.factcheck || {};

        const setz = (id, wert) => {
            const el = document.getElementById(id);
            if (el) el.textContent = wert || '';
        };
        setz('art-summary-meta', s.exists
            ? `${arts} Artikel ausgewertet, ${this._fmtStamp(s.last)}` : 'noch nicht erzeugt');
        setz('art-fc-meta', fc.exists
            ? `${fc.facts} Fakten, ${this._fmtStamp(fc.last)}` : 'noch nicht erzeugt');

        const snap = document.getElementById('art-snap-meta');
        if (snap && !snap.textContent) snap.textContent = f.snapshots ? String(f.snapshots) : '';
        const tl = document.getElementById('art-tl-meta');
        if (tl && !tl.textContent && f.events) tl.textContent = `${f.events} Ereignisse`;
    },

    async runStage(stage) {
        if (!this.incident) return;

        // Läuft genau dieser Baustein schon? -> Klick bedeutet Abbrechen
        if (this._runningStage === stage && this._CANCELLABLE.includes(stage)) {
            try {
                await API.cancelRefresh(this.incident.id);
                UI.showToast('Abbruch angefordert …', 'info');
            } catch (e) {
                UI.showToast((e && e.message) || 'Abbruch fehlgeschlagen', 'error');
            }
            return;
        }
        if (this._runningStage) {
            UI.showToast('Es läuft bereits ein Baustein für diese Lage.', 'info');
            return;
        }

        const label = this._STAGE_LABELS[stage] || 'Baustein';
        this._stageRunning(stage, 'startet …');
        try {
            if (stage === 'full') {
                // Beim leeren Fall ist es eine Recherche, sonst eine Aktualisierung -
                // dieselbe Aktion, aber der Nutzer soll den Namen wiedererkennen, den
                // er angeklickt hat.
                const erstlauf = !(this.articles || []).length;
                await API.refreshIncident(this.incident.id);
                this._showStatus('In Warteschlange ...');
                UI.showToast(erstlauf ? 'Recherche gestartet' : 'Kompletter Lauf gestartet', 'info');
            } else if (stage === 'geoparse') {
                await API.triggerGeoparse(this.incident.id);
                this._showStatus('Geoparsing läuft …');
                this._pollGeoparse();
            } else {
                await API.runStage(this.incident.id, stage);
                this._showStatus(label + ' läuft …');
                // 'collect' läuft über die Refresh-Pipeline -> Abschluss/Reload via WebSocket
                // (_wireWs: refresh_complete/refresh_error). Analyse/Faktencheck via Polling.
                if (stage !== 'collect') this._pollStage();
            }
            this._stageRunning(stage, 'läuft …');
        } catch (e) {
            this._stagesIdle();
            this._hideStatus();
            const busy = e && e.status === 409;
            UI.showToast(busy ? 'Es läuft bereits ein Baustein für diese Lage.'
                             : ((e && e.message) || 'Start fehlgeschlagen'), 'error');
        }
    },
    _pollStage() {
        if (this._stagePoll) return;
        this._stagePoll = setInterval(async () => {
            let res;
            try { res = await API.getRunStatus(this.incident.id); } catch (e) { return; }
            const st = res && res.state;
            if (!st) { this._stopStagePoll(); this._hideStatus(); this._stagesIdle(); return; }
            if (st.status === 'running') {
                this._showStatus((st.label || 'Baustein') + ' läuft …');
                this._stageRunning(st.stage || this._runningStage, 'läuft …');
            } else {
                this._stopStagePoll(); this._hideStatus(); this._stagesIdle();
                if (st.status === 'done') {
                    UI.showToast((st.label || 'Baustein') + ' fertig', 'info');
                    this.softRefresh();
                } else if (st.status === 'error') {
                    UI.showToast((st.label || 'Baustein') + ' fehlgeschlagen: ' + (st.error || ''), 'error');
                }
            }
        }, 2000);
    },
    _stopStagePoll() { if (this._stagePoll) { clearInterval(this._stagePoll); this._stagePoll = null; } },
    _pollGeoparse() {
        if (this._geoPoll) return;
        this._geoPoll = setInterval(async () => {
            let st;
            try { st = await API.getGeoparseStatus(this.incident.id); } catch (e) { return; }
            if (st && st.status === 'done') {
                this._stopGeoPoll(); this._hideStatus(); this._stagesIdle();
                UI.showToast('Geoparsing fertig · ' + (st.locations || 0) + ' Orte', 'info');
                this.softRefresh();
            } else if (st && st.status === 'error') {
                this._stopGeoPoll(); this._hideStatus(); this._stagesIdle();
                UI.showToast('Geoparsing fehlgeschlagen: ' + (st.error || ''), 'error');
            } else {
                // running / idle
                if (st && st.total) {
                    const prog = (st.processed || 0) + '/' + st.total;
                    this._showStatus('Geoparsing läuft … ' + prog);
                    this._stageRunning('geoparse', prog);
                }
            }
        }, 2000);
    },
    _stopGeoPoll() { if (this._geoPoll) { clearInterval(this._geoPoll); this._geoPoll = null; } },
    async _resumeStage() {
        // Beim Öffnen einer Lage: läuft dort gerade ein Baustein? -> Kachel + Polling aufnehmen
        this._stopStagePoll();
        this._stopGeoPoll();
        try {
            const res = await API.getRunStatus(this.incident.id);
            if (res && res.state && res.state.status === 'running') {
                this._showStatus((res.state.label || 'Baustein') + ' läuft …');
                this._stageRunning(res.state.stage, 'läuft …');
                this._pollStage();
                return;
            }
        } catch (e) { /* ignore */ }
        try {
            const g = await API.getGeoparseStatus(this.incident.id);
            if (g && g.status === 'running') {
                this._showStatus('Geoparsing läuft …');
                this._stageRunning('geoparse', 'läuft …');
                this._pollGeoparse();
            }
        } catch (e) { /* ignore */ }
    },

    // Soft-Refresh (F5): Daten neu laden statt die Seite neu zu laden (wie im Monitor)
    async softRefresh() {
        await this.loadIncidents();
        if (!this.incident) return;
        const openBefore = this.openTabs.slice();
        const activeBefore = this.activeTab;
        await this.loadDetail(this.incident.id);
        // geöffnete Tabs wiederherstellen (loadDetail öffnet sonst nur das Lagebild)
        if (openBefore.length) {
            this.openTabs = [];
            document.getElementById('center-tabbar').innerHTML = '';
            openBefore.forEach(k => this.openTab(k));
            if (activeBefore && this.openTabs.includes(activeBefore)) this.activateTab(activeBefore);
        }
        UI.showToast('Aktualisiert', 'info');
    },

    _showStatus(text) {
        const bar = document.getElementById('live-status');
        document.getElementById('live-status-text').textContent = text;
        bar.classList.add('active');
        if (!this._startTime) this._startTime = Date.now();
        if (!this._timerInt) {
            this._timerInt = setInterval(() => {
                const s = Math.floor((Date.now() - this._startTime) / 1000);
                document.getElementById('live-status-timer').textContent =
                    Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0');
            }, 1000);
        }
    },
    _hideStatus() {
        document.getElementById('live-status').classList.remove('active');
        if (this._timerInt) { clearInterval(this._timerInt); this._timerInt = null; }
        this._startTime = null;
        document.getElementById('live-status-timer').textContent = '';
    },

    _wireWs() {
        const forCurrent = (msg) => this.incident && String(msg.incident_id) === String(this.incident.id);
        WS.on('status_update', (msg) => {
            if (!forCurrent(msg)) return;
            const label = (typeof UI._getStepLabel === 'function') ? UI._getStepLabel(msg.status) : (msg.status || '');
            this._showStatus(msg.detail || label || 'Aktualisierung läuft ...');
        });
        WS.on('pipeline_step', (msg) => {
            if (!forCurrent(msg)) return;
            if (msg.label || msg.step) this._showStatus(msg.label || msg.step);
        });
        WS.on('refresh_complete', (msg) => {
            if (!forCurrent(msg)) return;
            const wasCollect = this._runningStage === 'collect';
            this._hideStatus();
            this._stagesIdle();
            UI.showToast(wasCollect ? 'Sammeln abgeschlossen' : 'Aktualisierung abgeschlossen', 'info');
            this.loadDetail(this.incident.id);
            this.loadIncidents();
        });
        WS.on('refresh_error', (msg) => {
            if (!forCurrent(msg)) return;
            this._hideStatus();
            this._stagesIdle();
            UI.showToast('Aktualisierung fehlgeschlagen', 'error');
        });
        WS.on('refresh_cancelled', (msg) => {
            if (!forCurrent(msg)) return;
            this._hideStatus();
            this._stagesIdle();
            UI.showToast('Abgebrochen', 'info');
        });
    },

    async syncRefreshStatus() {
        // Nach WS-Reconnect: laufende Refreshes abgleichen
        try {
            const running = await API.getRefreshingIncidents();
            const ids = (running || []).map(r => String(r.incident_id || r.id));
            if (this.incident && ids.includes(String(this.incident.id))) {
                this._showStatus('Aktualisierung läuft ...');
                // Welcher der beiden Orchestrator-Bausteine läuft, sagt uns der Server nicht;
                // ohne bekannten Zustand als "Kompletter Lauf" anzeigen (beide sind abbrechbar).
                this._stageRunning(this._runningStage || 'full', 'läuft …');
            } else {
                this._hideStatus();
                if (this._runningStage === 'collect' || this._runningStage === 'full') this._stagesIdle();
            }
        } catch (e) { /* ignore */ }
    },

    // === Neuen Fall anlegen (gleicher Funktionsumfang wie im Dashboard) ===
    openNewIncident() {
        const form = document.getElementById('new-incident-form');
        form.reset();
        document.getElementById('inc-visibility').checked = true;
        document.getElementById('btn-enhance-description').disabled = true;
        this.incTypeDefaults();
        this.incRefreshToggle();
        this.incVisibilityHint();
        this._gateXToggle();
        const m = document.getElementById('modal-new');
        m.classList.add('active');
        setTimeout(() => document.getElementById('inc-title').focus(), 50);
    },
    closeNewIncident() {
        document.getElementById('modal-new').classList.remove('active');
        if (this._enhanceController) { this._enhanceController.abort(); this._enhanceController = null; }
    },

    // X-Kanäle nur freigeben, wenn ein X-Zugang hinterlegt ist (wie im Dashboard)
    async _gateXToggle() {
        const cb = document.getElementById('inc-x');
        const hint = document.getElementById('inc-x-hint');
        if (!cb) return;
        let active = 0;
        try {
            const accounts = await API.listXAccounts();
            active = (accounts || []).filter(a => a.active).length;
        } catch (e) {
            active = 0;   // im Zweifel sperren
        }
        cb.disabled = active === 0;
        if (active === 0) cb.checked = false;
        if (hint) hint.style.display = active === 0 ? '' : 'none';
    },

    incTitleChanged() {
        const t = document.getElementById('inc-title').value.trim();
        document.getElementById('btn-enhance-description').disabled = t.length < 3;
    },

    incTypeDefaults() {
        const type = document.getElementById('inc-type').value;
        const hint = document.getElementById('type-hint');
        const mode = document.getElementById('inc-refresh-mode');
        if (type === 'research') {
            hint.textContent = 'Recherchiert in Tiefe: Nachrichtenarchive, Parlamentsdokumente, Fachmedien, Expertenquellen. Empfohlen: Manuell starten und bei Bedarf vertiefen.';
            mode.value = 'manual';
        } else {
            hint.textContent = 'Durchsucht laufend hunderte Nachrichtenquellen nach neuen Meldungen. Empfohlen: Automatische Aktualisierung.';
        }
        this.incRefreshToggle();
    },

    incRefreshToggle() {
        const auto = document.getElementById('inc-refresh-mode').value === 'auto';
        document.getElementById('refresh-interval-field').classList.toggle('visible', auto);
        document.getElementById('refresh-starttime-field').classList.toggle('visible', auto);
    },

    incIntervalMin() {
        const unit = parseInt(document.getElementById('inc-refresh-unit').value);
        const input = document.getElementById('inc-refresh-value');
        const min = unit === 1 ? 10 : 1;
        input.min = min;
        if (parseInt(input.value) < min) input.value = min;
    },

    incVisibilityHint() {
        const pub = document.getElementById('inc-visibility').checked;
        document.getElementById('visibility-text').textContent = pub
            ? 'Öffentlich : für alle Nutzer sichtbar'
            : 'Privat : nur für dich sichtbar';
    },

    async generateDescription() {
        const title = document.getElementById('inc-title').value.trim();
        const description = document.getElementById('inc-description').value.trim();
        const type = document.getElementById('inc-type').value;
        const btn = document.getElementById('btn-enhance-description');
        const btnText = document.getElementById('enhance-btn-text');
        const spinner = document.getElementById('enhance-spinner');
        const area = document.getElementById('inc-description');
        if (title.length < 3) return;

        if (this._enhanceController) this._enhanceController.abort();
        this._enhanceController = new AbortController();

        btn.disabled = true;
        btnText.textContent = 'Wird generiert …';
        spinner.style.display = '';
        area.readOnly = true;
        try {
            const res = await API.enhanceDescription(title, description || null, type, this._enhanceController.signal);
            area.value = res.description || '';
        } catch (err) {
            if (err && err.name !== 'AbortError') {
                let msg = 'Beschreibung konnte nicht generiert werden';
                if (err.status === 503) msg = 'KI-Zugang aktuell nicht verfügbar.';
                else if (err.status === 429) msg = 'KI ist gerade ausgelastet. Bitte kurz warten.';
                else if (err.status === 504) msg = 'KI antwortet gerade nicht. Bitte erneut versuchen.';
                UI.showToast(msg, 'error');
            }
        } finally {
            spinner.style.display = 'none';
            btnText.textContent = 'Beschreibung generieren';
            area.readOnly = false;
            btn.disabled = title.length < 3;
            this._enhanceController = null;
        }
    },

    _incidentFormData() {
        const value = parseInt(document.getElementById('inc-refresh-value').value) || 15;
        const unit = parseInt(document.getElementById('inc-refresh-unit').value) || 1;
        const auto = document.getElementById('inc-refresh-mode').value === 'auto';
        return {
            title: document.getElementById('inc-title').value.trim(),
            description: document.getElementById('inc-description').value.trim() || null,
            type: document.getElementById('inc-type').value,
            refresh_mode: document.getElementById('inc-refresh-mode').value,
            refresh_interval: Math.max(10, Math.min(10080, value * unit)),
            refresh_start_time: auto ? (document.getElementById('inc-refresh-starttime').value || null) : null,
            retention_days: parseInt(document.getElementById('inc-retention').value) || 0,
            international_sources: document.getElementById('inc-international').checked,
            include_telegram: document.getElementById('inc-telegram').checked,
            include_x: document.getElementById('inc-x').checked,
            visibility: document.getElementById('inc-visibility').checked ? 'public' : 'private',
            ai_backend: (document.getElementById('inc-ai-backend') || {}).value || '',
        };
    },

    async submitIncident(ev) {
        ev.preventDefault();
        const btn = document.getElementById('modal-new-submit');
        const titleEl = document.getElementById('inc-title');
        if (!titleEl.value.trim()) { titleEl.focus(); return; }

        btn.disabled = true;
        try {
            const inc = await API.createIncident(this._incidentFormData());
            // E-Mail-Abo wie im Dashboard mitspeichern
            try {
                await API.updateSubscription(inc.id, {
                    notify_email_summary: document.getElementById('inc-notify-summary').checked,
                    notify_email_new_articles: document.getElementById('inc-notify-new-articles').checked,
                    notify_email_status_change: document.getElementById('inc-notify-status-change').checked,
                });
            } catch (e) { /* Abo ist Beiwerk - der Fall steht */ }

            this.closeNewIncident();
            await this.loadIncidents();
            await this.selectIncident(inc.id);
            UI.showToast('Fall angelegt', 'success');
        } catch (e) {
            UI.showToast((e && e.message) || 'Fall konnte nicht angelegt werden', 'error');
        } finally {
            btn.disabled = false;
        }
    },

    // === Export ===
    async doExport() {
        if (!this.incident) return;
        const format = document.getElementById('export-format').value;
        const sections = ['zusammenfassung', 'bericht', 'faktencheck', 'quellen'];
        try {
            const resp = await API.exportReport(this.incident.id, format, null, sections);
            if (!resp.ok) throw new Error('Export fehlgeschlagen');
            const blob = await resp.blob();
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `Lagebericht_${this.incident.id}.${format}`;
            document.body.appendChild(a); a.click(); a.remove();
            URL.revokeObjectURL(url);
        } catch (e) {
            UI.showToast('Export fehlgeschlagen', 'error');
        }
    },

    // === Theme ===
    toggleTheme() {
        const cur = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
        const next = cur === 'light' ? 'dark' : 'light';
        if (next === 'light') document.documentElement.setAttribute('data-theme', 'light');
        else document.documentElement.removeAttribute('data-theme');
        localStorage.setItem('osint_theme', next);
        this._syncThemeSwitch();
        if (UI.updateMapTheme) UI.updateMapTheme();
    },

    // Der Schiebeschalter aus dem klassischen Dashboard zeigt seinen Zustand ueber
    // die Klasse 'dark' bzw. 'light'.
    _syncThemeSwitch() {
        const el = document.getElementById('theme-toggle');
        if (!el) return;
        const theme = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
        el.classList.remove('dark', 'light');
        el.classList.add(theme);
        el.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false');
    },

    // === Zeit-Helfer ===
    _ts(a) {
        const d = parseUTC(a.published_at || a.collected_at || a.created_at);
        return d ? d.getTime() : 0;
    },
    _fmtDate(a, withTime) {
        const d = parseUTC(a.published_at || a.collected_at || a.created_at);
        return this._fmt(d, withTime);
    },
    _fmt(d, withTime) {
        if (!d) return '';
        const opts = withTime
            ? { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }
            : { day: '2-digit', month: '2-digit' };
        return d.toLocaleString('de-DE', opts);
    },
};

document.addEventListener('DOMContentLoaded', () => Studio.init());

// F5: Daten aktualisieren statt Seite neu laden (wie im Monitor)
document.addEventListener('keydown', (e) => {
    if (e.key === 'F5') {
        e.preventDefault();
        Studio.softRefresh();
    }
    // ESC schließt Rueckfrage, Anlege-Dialog, Kontextmenue bzw. den Auswahlmodus
    if (e.key === 'Escape') {
        const c = document.getElementById('modal-confirm');
        if (c && c.classList.contains('active')) { Studio._confirmClose(false); return; }
        const m = document.getElementById('modal-new');
        if (m && m.classList.contains('active')) Studio.closeNewIncident();
        Studio._closeCaseMenu();
        if (Studio.caseMode !== null) Studio.endCaseMode();
    }
});
