/**
 * Spellmatic — Admin Dashboard JS  (Phase 2 / 4)
 *
 * Scan flow:
 *   1. POST /scan/start  → PHP returns { scan_id, total_items, items, post_meta }
 *   2. Wait for window.__spellmaticHarper.ready  (Harper WASM, loaded by harper-bridge.js)
 *   3. For each item: harper.lintText(text)  → collect errors
 *   4. POST /scan/submit { scan_id, errors }  → PHP stores results and returns final list
 *   5. Render results, enable fix / dismiss buttons
 *
 * Phase 4 additions:
 *   - Custom Dictionary panel: add / remove words via REST; updates rendered tag list
 *   - Scan Digest panel: save schedule settings + send test email via REST
 */

( function() {
    'use strict';

    // ── Guard: ensure localized data is present ───────────────────────────────
    if ( ! window.spellmatic ) {
        console.error( 'Spellmatic: spellmatic config not found. Script may have loaded too early.' );
        return;
    }

    const { nonce, restUrl, adminUrl, strings } = window.spellmatic;
    const digestConfig = window.spellmatic.digest   || {};
    const byokConfig   = window.spellmatic.byok     || {};
    const postTypes    = window.spellmatic.postTypes || {}; // slug → label

    // Backup warning — shown once before the very first fix is applied.
    let backupDismissed = !! window.spellmatic.backupDismissed;

    let scanId   = null;
    let postMeta = {}; // keyed by post_id — populated from scan/start response
    let autoScanPostId = 0; // Set when arriving from "Spell Check" row action
    let singlePostScanId = 0; // Tracks the post ID during a single-post scan

    // Grouped results state
    const PAGE_SIZE       = 10;  // post groups per page
    let allPostGroups     = [];  // full grouped data after scan
    let filteredGroups    = [];  // after filter/sort applied
    let currentPage       = 1;

    // Results-page state (hoisted so handleAddToDictionary can purge across pages)
    let _rpAllGroups      = null;
    let _rpFilteredGroups = null;
    let _rpRenderPage     = null;  // function ref set by initResultsPage()

    // DOM refs
    let scanBtn, progressWrap, progressFill, progressText,
        resultsContainer, resultsList, resultsTitle, resultsSummary,
        filterBar, filterSearch, filterType, filterSort, paginationEl;

    // ── Init ──────────────────────────────────────────────────────────────────
    function init() {
        scanBtn          = document.getElementById( 'spellmatic-scan-btn' );
        progressWrap     = document.getElementById( 'spellmatic-progress' );
        progressFill     = document.getElementById( 'spellmatic-progress-fill' );
        progressText     = document.getElementById( 'spellmatic-progress-text' );
        resultsContainer = document.getElementById( 'spellmatic-results-container' );
        resultsList      = document.getElementById( 'spellmatic-results-list' );
        resultsTitle     = document.getElementById( 'spellmatic-results-title' );
        filterBar        = document.getElementById( 'spellmatic-filter-bar' );
        filterSearch     = document.getElementById( 'spellmatic-filter-search' );
        filterType       = document.getElementById( 'spellmatic-filter-type' );
        filterSort       = document.getElementById( 'spellmatic-filter-sort' );
        paginationEl     = document.getElementById( 'spellmatic-pagination' );
        resultsSummary   = document.getElementById( 'spellmatic-results-summary' );

        if ( scanBtn ) {
            scanBtn.addEventListener( 'click', startScan );
        }

        // Wire result actions — scan page uses #spellmatic-results-list,
        // the dedicated Results page uses #spellmatic-results-table-wrap.
        if ( resultsList ) resultsList.addEventListener( 'click', handleResultAction );
        const resultsTableWrap = document.getElementById( 'spellmatic-results-table-wrap' );
        if ( resultsTableWrap ) resultsTableWrap.addEventListener( 'click', handleResultAction );

        // Results page: bulk Dismiss All button.
        const resultsBulkDismissBtn = document.getElementById( 'spellmatic-results-bulk-dismiss-btn' );
        if ( resultsBulkDismissBtn ) resultsBulkDismissBtn.addEventListener( 'click', handleResultsPageBulkDismiss );

        // Results page: per-page selector redirects to new URL.
        const perPageSel = document.getElementById( 'spellmatic-results-per-page' );
        if ( perPageSel ) {
            perPageSel.addEventListener( 'change', function() {
                const base = perPageSel.dataset.baseUrl || '';
                window.location.href = base + '&per_page=' + encodeURIComponent( perPageSel.value ) + '&paged=1';
            } );
        }

        // Results page: Clear All button.
        const clearResultsBtn = document.getElementById( 'spellmatic-clear-results-btn' );
        if ( clearResultsBtn ) {
            clearResultsBtn.addEventListener( 'click', async function() {
                const msg = clearResultsBtn.dataset.confirm || 'Clear all scan results and history? This cannot be undone.';
                if ( ! confirm( msg ) ) return;
                clearResultsBtn.disabled    = true;
                clearResultsBtn.textContent = 'Clearing…';
                try {
                    await apiFetch( restUrl + '/results/clear', { method: 'POST' } );
                    window.location.reload();
                } catch ( err ) {
                    clearResultsBtn.disabled    = false;
                    clearResultsBtn.innerHTML   = '<span class="dashicons dashicons-trash"></span> Clear All';
                    showError( err.message || 'Could not clear results.' );
                }
            } );
        }

        // Scope selector: build post-type checkboxes from localized data.
        initScopeSelector();

        // Single-post search selector.
        initPostSearch();

        // Phase 4: boot settings-page panels if their containers exist.
        if ( document.getElementById( 'spellmatic-dict-panel' ) ) {
            initDictionaryPanel();
            initDictExportImport();
        }
        if ( document.getElementById( 'spellmatic-digest-panel' ) ) {
            initDigestPanel();
        }

        // Phase 5: wire up BYOK provider test buttons + AI toggle.
        initByokTestButtons();
        initAiToggle();

        // Settings page: live cost-estimate preview next to Items per Scan input.
        initAiCostPreview();

        // Results page: render grouped cards from server-provided data.
        if ( window.spellmaticResults ) {
            initResultsPage();
        }

        // Auto-scan a single post when arriving from the "Spell Check" row action.
        var autoScanPost = parseInt( window.spellmatic.autoScanPost || 0, 10 );
        if ( autoScanPost && scanBtn ) {
            autoScanPostId = autoScanPost;
            updateScanBtnLabel();

            // Show the selected-post chip so the user knows what's being scanned.
            var selectedBox = document.getElementById( 'spellmatic-post-search-selected' );
            var postTitle   = window.spellmatic.autoScanPostTitle || ( 'Post #' + autoScanPost );
            if ( selectedBox ) {
                selectedBox.innerHTML =
                    '<span class="dashicons dashicons-media-text" style="color:#2271b1;"></span>' +
                    '<span>' + escHtml( postTitle ) + '</span>' +
                    '<button type="button" class="spellmatic-clear-selection" title="Clear selection">&times;</button>';
                selectedBox.hidden = false;
            }

            // Open the scope panel so the chip is visible.
            var scopePanel  = document.getElementById( 'spellmatic-scope-panel' );
            var scopeToggle = document.getElementById( 'spellmatic-scope-toggle' );
            if ( scopePanel ) scopePanel.hidden = false;
            if ( scopeToggle ) scopeToggle.setAttribute( 'aria-expanded', 'true' );

            // Small delay to let the page render before starting.
            setTimeout( function() { startScan(); }, 300 );
        }

        if ( window.spellmatic && window.spellmatic.debug ) {
            console.log( 'Spellmatic dashboard initialised. REST URL:', restUrl );
        }
    }

    // ── Scope selector ────────────────────────────────────────────────────────
    function initScopeSelector() {
        const toggleBtn  = document.getElementById( 'spellmatic-scope-toggle' );
        const panel      = document.getElementById( 'spellmatic-scope-panel' );
        const typesWrap  = document.getElementById( 'spellmatic-scope-types' );
        const selectAll  = document.getElementById( 'spellmatic-scope-all' );

        if ( ! toggleBtn || ! panel || ! typesWrap ) return;

        // Build checkboxes from postTypes.
        const saved = getScopeSelection();
        Object.keys( postTypes ).forEach( function( slug ) {
            const label    = postTypes[ slug ];
            const checked  = saved === null || saved.indexOf( slug ) !== -1;
            const id       = 'spellmatic-scope-' + slug;
            const wrapper  = document.createElement( 'label' );
            wrapper.className = 'spellmatic-scope-item';
            wrapper.innerHTML =
                '<input type="checkbox" id="' + escAttr( id ) + '" ' +
                'name="spellmatic_scope[]" value="' + escAttr( slug ) + '"' +
                ( checked ? ' checked' : '' ) + '> ' +
                escHtml( label );
            typesWrap.appendChild( wrapper );
        } );

        // Toggle panel open/close.
        toggleBtn.addEventListener( 'click', function() {
            const open = panel.hidden;
            panel.hidden = ! open;
            toggleBtn.setAttribute( 'aria-expanded', open ? 'true' : 'false' );
            toggleBtn.querySelector( '.spellmatic-scope-chevron' ).style.transform =
                open ? 'rotate(90deg)' : '';
        } );

        // "Select all" master checkbox.
        if ( selectAll ) {
            selectAll.addEventListener( 'change', function() {
                typesWrap.querySelectorAll( 'input[type=checkbox]' ).forEach( function( cb ) {
                    cb.checked = selectAll.checked;
                } );
                saveScopeSelection();
            } );
        }

        // Individual checkbox changes.
        typesWrap.addEventListener( 'change', function() {
            saveScopeSelection();
            if ( selectAll ) {
                const all  = typesWrap.querySelectorAll( 'input[type=checkbox]' );
                const chkd = typesWrap.querySelectorAll( 'input[type=checkbox]:checked' );
                selectAll.checked       = chkd.length === all.length;
                selectAll.indeterminate = chkd.length > 0 && chkd.length < all.length;
            }
        } );

        updateScanBtnLabel();
    }

    function getSelectedPostTypes() {
        const wrap = document.getElementById( 'spellmatic-scope-types' );
        if ( ! wrap ) return Object.keys( postTypes );
        const checked = wrap.querySelectorAll( 'input[type=checkbox]:checked' );
        return Array.from( checked ).map( function( cb ) { return cb.value; } );
    }

    function getScopeSelection() {
        try {
            const raw = localStorage.getItem( 'spellmatic_scope' );
            return raw ? JSON.parse( raw ) : null;
        } catch(e) { return null; }
    }

    function saveScopeSelection() {
        try {
            localStorage.setItem( 'spellmatic_scope', JSON.stringify( getSelectedPostTypes() ) );
        } catch(e) {}
        updateScanBtnLabel();
    }

    // ── Single-post search selector ──────────────────────────────────────────
    function initPostSearch() {
        var searchInput   = document.getElementById( 'spellmatic-post-search' );
        var resultsBox    = document.getElementById( 'spellmatic-post-search-results' );
        var selectedBox   = document.getElementById( 'spellmatic-post-search-selected' );
        if ( ! searchInput || ! resultsBox || ! selectedBox ) return;

        var debounceTimer = null;

        searchInput.addEventListener( 'input', function() {
            clearTimeout( debounceTimer );
            var q = searchInput.value.trim();
            if ( q.length < 2 ) { resultsBox.hidden = true; resultsBox.innerHTML = ''; return; }
            debounceTimer = setTimeout( function() { doSearch( q ); }, 300 );
        } );

        // Close dropdown when clicking outside.
        document.addEventListener( 'click', function( e ) {
            if ( ! searchInput.contains( e.target ) && ! resultsBox.contains( e.target ) ) {
                resultsBox.hidden = true;
            }
        } );

        async function doSearch( query ) {
            try {
                // Search across all public post types via WP REST API.
                // Use WP REST root (restUrl is .../spellmatic/v1, go up to wp-json root).
                var wpRestRoot = restUrl.replace( /spellmatic\/v1\/?$/, '' );
                var resp = await apiFetch( wpRestRoot + 'wp/v2/search?search=' +
                    encodeURIComponent( query ) + '&per_page=8&type=post&subtype=any' );
                if ( ! Array.isArray( resp ) || resp.length === 0 ) {
                    resultsBox.innerHTML = '<div class="spellmatic-post-search-item" style="color:#787c82;cursor:default;">No results found</div>';
                    resultsBox.hidden = false;
                    return;
                }
                resultsBox.innerHTML = resp.map( function( item ) {
                    return '<div class="spellmatic-post-search-item" data-post-id="' + item.id + '" data-post-title="' + escAttr( item.title ) + '" data-post-type="' + escAttr( item.subtype || 'post' ) + '">' +
                        '<span>' + escHtml( item.title ) + '</span>' +
                        '<span class="spellmatic-pt-badge">' + escHtml( item.subtype || 'post' ) + '</span>' +
                    '</div>';
                } ).join( '' );
                resultsBox.hidden = false;
            } catch( err ) {
                resultsBox.innerHTML = '<div class="spellmatic-post-search-item" style="color:#d63638;cursor:default;">Search failed</div>';
                resultsBox.hidden = false;
            }
        }

        // Handle clicking a search result.
        resultsBox.addEventListener( 'click', function( e ) {
            var item = e.target.closest( '.spellmatic-post-search-item' );
            if ( ! item || ! item.dataset.postId ) return;

            var postId    = parseInt( item.dataset.postId, 10 );
            var postTitle = item.dataset.postTitle || '';
            var postType  = item.dataset.postType  || 'post';

            // Set the single-post scan target.
            autoScanPostId = postId;
            window.spellmatic.autoScanPostTitle = postTitle;

            // Show selected state.
            selectedBox.innerHTML =
                '<span class="dashicons dashicons-media-text" style="color:#2271b1;"></span>' +
                '<span>' + escHtml( postTitle ) + '</span>' +
                '<span class="spellmatic-pt-badge">' + escHtml( postType ) + '</span>' +
                '<button type="button" class="spellmatic-clear-selection" title="Clear selection">&times;</button>';
            selectedBox.hidden = false;

            // Clear and hide search.
            searchInput.value = '';
            resultsBox.hidden = true;
            resultsBox.innerHTML = '';

            updateScanBtnLabel();
        } );

        // Handle clearing the selection.
        selectedBox.addEventListener( 'click', function( e ) {
            if ( ! e.target.closest( '.spellmatic-clear-selection' ) ) return;
            autoScanPostId = 0;
            window.spellmatic.autoScanPostTitle = '';
            selectedBox.hidden = true;
            selectedBox.innerHTML = '';
            updateScanBtnLabel();
        } );
    }

    function updateScanBtnLabel() {
        if ( ! scanBtn ) return;
        const icon = '<span class="dashicons dashicons-search"></span> ';

        // Single-post scan mode (from "Spell Check" row action or post selector).
        if ( autoScanPostId > 0 ) {
            var postTitle = window.spellmatic.autoScanPostTitle || '';
            scanBtn.innerHTML = icon + 'Scan' + ( postTitle ? ' \u201C' + escHtml( postTitle ) + '\u201D' : ' Single Post' );
            scanBtn.disabled  = false;
            return;
        }

        const selected = getSelectedPostTypes();
        const total    = Object.keys( postTypes ).length;
        if ( ! total || selected.length === total ) {
            scanBtn.innerHTML = icon + 'Scan Site';
        } else if ( selected.length === 0 ) {
            scanBtn.innerHTML = icon + 'Select a content type to scan';
            scanBtn.disabled  = true;
            return;
        } else {
            const labels = selected.map( function(s) { return postTypes[s] || s; } );
            scanBtn.innerHTML = icon + 'Scan ' + escHtml( labels.join( ', ' ) );
        }
        scanBtn.disabled = false;
    }

    // WordPress footer scripts run AFTER DOMContentLoaded fires, so check readyState.
    if ( document.readyState === 'loading' ) {
        document.addEventListener( 'DOMContentLoaded', init );
    } else {
        init();
    }

    // ── Main scan entry-point ────────────────────────────────────────────────
    async function startScan() {
        scanBtn.disabled    = true;
        scanBtn.textContent = strings.scanning || 'Scanning…';
        clearError();
        showProgress( 0, strings.scanning || 'Starting scan…' );

        try {

            // ── Step 1: Ask PHP for the list of items to scan ──────────────
            showProgress( 2, 'Preparing item list…' );
            const selectedTypes = getSelectedPostTypes();
            const scanPayload   = autoScanPostId
                ? { scan_post_ids: [ autoScanPostId ] }
                : { post_types: selectedTypes };
            // Track single-post context for onScanComplete.
            // Keep autoScanPostId so the button label + chip persist after the scan.
            singlePostScanId = autoScanPostId;

            const startData = await apiFetch( restUrl + '/scan/start', {
                method:  'POST',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify( scanPayload ),
            } );

            if ( ! startData.scan_id || ! Array.isArray( startData.items ) ) {
                throw new Error( startData.error || 'Scan could not start.' );
            }

            scanId   = startData.scan_id;
            window.spellmatic._scanId = scanId;
            postMeta = startData.post_meta || {};
            const items = startData.items;
            const total = items.length;

            // ── Step 2: Wait for Harper WASM to be ready ───────────────────
            showProgress( 3, 'Loading grammar engine…' );
            const harper = await waitForHarper();

            // ── Step 3: Lint each item in the browser ──────────────────────
            const allErrors = [];

            for ( let i = 0; i < total; i++ ) {
                const item     = items[ i ];
                const label    = item.post_type + ' › ' + item.field.replace( 'post_', '' );
                const pct      = 5 + Math.round( ( i / total ) * 85 );   // 5 → 90 %
                showProgress( pct, 'Checking ' + label + ' (' + ( i + 1 ) + ' / ' + total + ')…' );

                try {
                    const lints = await harper.lintText( item.text );
                    for ( const lint of lints ) {
                        allErrors.push( {
                            post_id:      item.post_id,
                            post_type:    item.post_type,
                            field:        item.field,
                            type:         lint.type,
                            original:     lint.original,
                            suggestion:   lint.suggestion,
                            context:      lint.context,
                            offset_start: lint.offset_start,
                            offset_end:   lint.offset_end,
                        } );
                    }
                } catch ( lintErr ) {
                    console.warn( 'Spellmatic: lint error for item', item.id, lintErr );
                }
            }

            // ── Step 4: Submit errors to PHP for storage ───────────────────
            showProgress( 92, 'Saving results…' );
            const submitData = await apiFetch( restUrl + '/scan/submit', {
                method:  'POST',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify( { scan_id: scanId, errors: allErrors } ),
            } );

            // ── Step 5: Render Harper results ──────────────────────────────
            onScanComplete( submitData );

            // ── Step 6: AI supplemental pass (if provider key set AND AI enabled) ─
            if ( byokConfig.activeProvider && byokConfig.aiEnabled ) {
                await runAiPass( items );
            }

        } catch ( err ) {
            console.error( 'Spellmatic scan error:', err );
            showError( err.message || 'An error occurred. Check the browser console for details.' );
            resetScanBtn();
        }
    }

    // ── Scan complete ─────────────────────────────────────────────────────────
    function onScanComplete( data ) {
        const newErrors   = data.error_count        || 0;
        const dupeSkipped = data.duplicates_skipped  || 0;
        const totalOpen   = data.total_found         || ( data.results ? data.results.length : 0 );
        const totalFound  = newErrors + dupeSkipped;

        const label = totalFound === 1
            ? ( strings.errorFound  || 'error found' )
            : ( strings.errorsFound || 'errors found' );

        showProgress( 100, ( strings.scanComplete || 'Scan complete!' ) + ' \u2014 ' + totalFound + ' ' + label );
        resetScanBtn();

        // \u2500\u2500 Single-post scan: filter results to just that post \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
        if ( singlePostScanId > 0 ) {
            var allResults = data.results || [];
            var postResults = allResults.filter( function( r ) {
                return parseInt( r.post_id, 10 ) === singlePostScanId;
            });
            var postId = singlePostScanId;
            singlePostScanId = 0;

            // Update progress label to reflect the single-post count.
            var postLabel = postResults.length === 1 ? 'error' : 'errors';
            showProgress( 100, ( strings.scanComplete || 'Scan complete!' ) + ' \u2014 ' + postResults.length + ' open ' + postLabel + ' for this post' );

            if ( postResults.length > 0 ) {
                renderResults( postResults, data );
            } else {
                showNoErrors();
            }
            return;
        }

        // Re-scan where every error already existed \u2014 no new inserts.
        if ( newErrors === 0 && dupeSkipped > 0 ) {
            showNoNewErrors( totalOpen );
        } else if ( totalFound === 0 ) {
            showNoErrors();
        } else {
            renderResults( data.results || [], data );
        }
    }

    /**
     * Show a message when a re-scan finds no NEW errors but open errors already exist.
     */
    function showNoNewErrors( totalOpen ) {
        if ( resultsContainer ) resultsContainer.classList.remove( 'hidden' );
        if ( resultsTitle )     resultsTitle.textContent = 'No new errors found.';
        var resultsUrl = ( adminUrl || '' ) + 'admin.php?page=spellmatic-results';
        if ( resultsList ) {
            resultsList.innerHTML =
                '<div class="spellmatic-no-new-errors">' +
                    '<p>&#10003; No new errors were found in this scan.</p>' +
                    '<p><strong>' + totalOpen + ' existing error' + ( totalOpen === 1 ? '' : 's' ) + '</strong> ' +
                    ( totalOpen === 1 ? 'is' : 'are' ) + ' still open from previous scans. ' +
                    '<a href="' + escAttr( resultsUrl ) + '" class="button button-primary">View Results →</a></p>' +
                '</div>';
        }
    }

    // ── Render results (grouped by post) ──────────────────────────────────────
    function renderResults( results, scanData ) {
        if ( ! resultsContainer || ! resultsList ) return;

        resultsContainer.classList.remove( 'hidden' );
        const count = results.length;

        if ( resultsTitle ) {
            resultsTitle.textContent = count + ' ' + ( count === 1
                ? ( strings.errorFound  || 'error found' )
                : ( strings.errorsFound || 'errors found' ) );
        }

        if ( count === 0 ) { showNoErrors(); return; }

        // ── Group results by post_id ───────────────────────────────────────
        const groupMap = {};
        results.forEach( function( r ) {
            const pid = String( r.post_id );
            if ( ! groupMap[ pid ] ) {
                groupMap[ pid ] = {
                    postId:    pid,
                    postTitle: r.post_title || ( ucfirst( r.post_type || 'post' ) + ' #' + pid ),
                    postType:  r.post_type  || 'post',
                    pm:        postMeta[ pid ] || {},
                    results:   [],
                };
            }
            groupMap[ pid ].results.push( r );
        } );

        allPostGroups  = Object.values( groupMap );
        filteredGroups = allPostGroups.slice();
        currentPage    = 1;

        // ── Results summary: "across N posts • M content types" ───────────
        if ( resultsSummary ) {
            const postCount = allPostGroups.length;
            const typeCount = new Set( allPostGroups.map( function(g) { return g.postType; } ) ).size;
            resultsSummary.textContent =
                'across ' + postCount + ' post' + ( postCount !== 1 ? 's' : '' ) +
                ' \u2022 ' + typeCount + ' content type' + ( typeCount !== 1 ? 's' : '' );
        }

        // ── Populate filter-bar type dropdown ─────────────────────────────
        initFilterBar();

        // ── Render first page ──────────────────────────────────────────────
        renderCurrentPage();
    }

    // ── Build one post-group card ──────────────────────────────────────────────
    function buildGroupCard( group ) {
        const pid       = group.postId;
        const pm        = group.pm;
        const errCount  = group.results.length;
        const typeLabel = postTypes[ group.postType ] || ucfirst( group.postType );

        const editUrl  = escAttr( adminUrl + 'post.php?post=' + pid + '&action=edit' );
        const viewUrl  = pm.permalink         ? escAttr( pm.permalink )         : '';
        const bldrUrl  = pm.builder_edit_url  ? escAttr( pm.builder_edit_url )  : '';
        const bldrLabel= pm.builder_edit_label? escHtml( pm.builder_edit_label ): '';

        const postLinks =
            ( viewUrl ? '<a href="' + viewUrl + '" target="_blank" class="spellmatic-post-link">View</a> <span class="spellmatic-link-sep">&middot;</span> ' : '' ) +
            '<a href="' + editUrl + '" target="_blank" class="spellmatic-post-link">Edit</a>' +
            ( bldrUrl ? ' <span class="spellmatic-link-sep">&middot;</span> <a href="' + bldrUrl + '" target="_blank" class="spellmatic-post-link">' + bldrLabel + '</a>' : '' );

        const errorRows = group.results.map( function( r ) {
            return buildErrorRow( r );
        } ).join( '' );

        return (
            '<div class="spellmatic-post-group" data-post-id="' + escAttr( pid ) + '" ' +
                 'data-post-type="' + escAttr( group.postType ) + '" ' +
                 'data-error-count="' + errCount + '">' +

                '<div class="spellmatic-post-group-header">' +
                    '<button class="spellmatic-group-toggle" aria-expanded="true" title="Collapse">' +
                        '<span class="spellmatic-toggle-icon">&#9660;</span>' +
                    '</button>' +
                    '<span class="spellmatic-type-badge spellmatic-type-badge--' + escAttr( group.postType ) + '">' +
                        escHtml( typeLabel ) +
                    '</span>' +
                    '<span class="spellmatic-group-title">' + escHtml( group.postTitle ) + '</span>' +
                    '<span class="spellmatic-group-count">' + errCount + ' error' + ( errCount !== 1 ? 's' : '' ) + '</span>' +
                    '<span class="spellmatic-post-links">' + postLinks + '</span>' +
                '</div>' +

                '<div class="spellmatic-post-group-body">' +
                    errorRows +
                '</div>' +

            '</div>'
        );
    }

    // ── Confidence check (mirrors PHP Spellmatic_Fixer::is_high_confidence) ──
    function isHighConfidence( r ) {
        var suggestion = r.suggestion || '';
        var errorType  = r.error_type || '';
        var original   = r.original   || '';
        if ( ! suggestion ) return false;
        if ( errorType !== 'spelling' ) return false;
        // Simple Levenshtein approximation for JS.
        var a = original.toLowerCase(), b = suggestion.toLowerCase();
        if ( a === b ) return true;
        var matrix = [];
        for ( var i = 0; i <= a.length; i++ ) {
            matrix[i] = [i];
            for ( var j = 1; j <= b.length; j++ ) {
                if ( i === 0 ) { matrix[i][j] = j; }
                else {
                    var cost = a[i-1] === b[j-1] ? 0 : 1;
                    matrix[i][j] = Math.min(
                        matrix[i-1][j] + 1,
                        matrix[i][j-1] + 1,
                        matrix[i-1][j-1] + cost
                    );
                }
            }
        }
        return matrix[a.length][b.length] <= 3;
    }

    // ── Build one error row (no post-title header — it's on the group card) ─────
    function buildErrorRow( r ) {
        const fieldLabel = formatFieldLabel( r.field || '' );
        const errorType  = r.error_type || 'spelling';
        const original   = r.original   || '';
        const suggestion = r.suggestion || '';
        const context    = r.context    || '';

        const highlightedContext = context
            ? escHtml( context ).replace(
                new RegExp( '(' + escRegex( original ) + ')', 'gi' ),
                '<mark class="spellmatic-error-mark">$1</mark>'
              )
            : '';

        return (
            '<div class="spellmatic-result-row" data-id="' + escAttr( r.id ) + '" data-suggestion="' + escAttr( suggestion ) + '" data-original="' + escAttr( original ) + '">' +

                '<div class="spellmatic-result-meta">' +
                    '<span class="spellmatic-badge spellmatic-badge--' + escAttr( errorType ) + '">' +
                        ucfirst( errorType ) +
                    '</span>' +
                    ( isHighConfidence( r )
                        ? '<span class="spellmatic-badge spellmatic-badge--confidence-high" title="Spelling error with a close suggestion — safe to auto-fix">High</span>'
                        : '<span class="spellmatic-badge spellmatic-badge--confidence-low" title="Grammar, style, or ambiguous — review manually">Low</span>'
                    ) +
                    '<span class="spellmatic-location">' + escHtml( fieldLabel ) + '</span>' +
                '</div>' +

                ( highlightedContext
                    ? '<div class="spellmatic-result-context">' +
                        '<span class="spellmatic-context-label">Context:</span> ' +
                        '&ldquo;' + highlightedContext + '&rdquo;' +
                      '</div>'
                    : '' ) +

                '<div class="spellmatic-fix-row">' +
                    '<span class="spellmatic-original-word">' + escHtml( original ) + '</span>' +
                    '<span class="spellmatic-arrow-icon">&#8594;</span>' +
                    '<input type="text" class="spellmatic-fix-input" ' +
                        'value="' + escAttr( suggestion ) + '" ' +
                        'placeholder="' + ( suggestion ? escAttr( suggestion ) : 'type correction…' ) + '" ' +
                        'data-original="' + escAttr( original ) + '" ' +
                        'aria-label="Correction for ' + escAttr( original ) + '" />' +
                    '<button class="button button-primary spellmatic-fix-btn" ' +
                        'data-id="' + escAttr( r.id ) + '" data-action="fix">Apply Fix</button>' +
                    '<button class="button spellmatic-dismiss-btn" ' +
                        'data-id="' + escAttr( r.id ) + '" data-action="dismiss">Dismiss</button>' +
                    '<button class="button spellmatic-dict-add-inline" ' +
                        'data-word="' + escAttr( original ) + '" data-action="add-to-dictionary" ' +
                        'title="Add to dictionary and dismiss all occurrences">' +
                        '<span class="dashicons dashicons-book" style="font-size:14px;line-height:1.8;margin-right:2px;"></span>Dictionary</button>' +
                '</div>' +

            '</div>'
        );
    }

    // ── Filter bar ────────────────────────────────────────────────────────────
    function initFilterBar() {
        if ( ! filterBar ) return;

        // Populate type dropdown with types present in results.
        if ( filterType ) {
            // Keep "All content types" option, add ones found in results.
            while ( filterType.options.length > 1 ) filterType.remove( 1 );
            const seenTypes = {};
            allPostGroups.forEach( function(g) { seenTypes[ g.postType ] = true; } );
            Object.keys( seenTypes ).forEach( function( slug ) {
                const opt = document.createElement( 'option' );
                opt.value       = slug;
                opt.textContent = postTypes[ slug ] || ucfirst( slug );
                filterType.appendChild( opt );
            } );
        }

        // Wire up filter controls.
        [ filterSearch, filterType, filterSort ].forEach( function( el ) {
            if ( el ) el.addEventListener( 'input', applyFiltersAndRender );
        } );

        filterBar.classList.remove( 'hidden' );
    }

    function applyFiltersAndRender() {
        const search  = filterSearch ? filterSearch.value.trim().toLowerCase() : '';
        const type    = filterType   ? filterType.value   : '';
        const sort    = filterSort   ? filterSort.value   : 'errors-desc';

        filteredGroups = allPostGroups.filter( function( g ) {
            if ( type   && g.postType !== type )                          return false;
            if ( search && g.postTitle.toLowerCase().indexOf( search ) === -1 ) return false;
            return true;
        } );

        if ( sort === 'errors-asc' ) {
            filteredGroups.sort( function(a,b) { return a.results.length - b.results.length; } );
        } else if ( sort === 'alpha' ) {
            filteredGroups.sort( function(a,b) { return a.postTitle.localeCompare( b.postTitle ); } );
        } else {
            filteredGroups.sort( function(a,b) { return b.results.length - a.results.length; } );
        }

        currentPage = 1;
        renderCurrentPage();
    }

    // ── Pagination ────────────────────────────────────────────────────────────
    function renderCurrentPage() {
        if ( ! resultsList ) return;

        const start  = ( currentPage - 1 ) * PAGE_SIZE;
        const page   = filteredGroups.slice( start, start + PAGE_SIZE );

        resultsList.innerHTML = page.map( buildGroupCard ).join( '' );

        // Wire up group collapse toggles.
        resultsList.querySelectorAll( '.spellmatic-group-toggle' ).forEach( function( btn ) {
            btn.addEventListener( 'click', function() {
                const body    = btn.closest( '.spellmatic-post-group' )
                                   .querySelector( '.spellmatic-post-group-body' );
                const open    = body.style.display !== 'none';
                body.style.display = open ? 'none' : '';
                btn.setAttribute( 'aria-expanded', open ? 'false' : 'true' );
                btn.querySelector( '.spellmatic-toggle-icon' ).textContent = open ? '▶' : '▼';
            } );
        } );

        renderPagination();
    }

    function renderPagination() {
        if ( ! paginationEl ) return;
        const totalPages = Math.ceil( filteredGroups.length / PAGE_SIZE );

        if ( totalPages <= 1 ) {
            paginationEl.classList.add( 'hidden' );
            paginationEl.innerHTML = '';
            return;
        }

        paginationEl.classList.remove( 'hidden' );

        let html = '';

        html += '<button class="spellmatic-page-btn" data-page="' + ( currentPage - 1 ) + '" ' +
                ( currentPage === 1 ? 'disabled' : '' ) + '>&laquo; Prev</button> ';

        for ( let p = 1; p <= totalPages; p++ ) {
            html += '<button class="spellmatic-page-btn' + ( p === currentPage ? ' is-current' : '' ) + '" data-page="' + p + '">' + p + '</button> ';
        }

        html += '<button class="spellmatic-page-btn" data-page="' + ( currentPage + 1 ) + '" ' +
                ( currentPage === totalPages ? 'disabled' : '' ) + '>Next &raquo;</button>';

        paginationEl.innerHTML = html;

        paginationEl.querySelectorAll( '.spellmatic-page-btn:not([disabled])' ).forEach( function( btn ) {
            btn.addEventListener( 'click', function() {
                currentPage = parseInt( btn.dataset.page, 10 );
                renderCurrentPage();
                resultsContainer.scrollIntoView( { behavior: 'smooth', block: 'start' } );
            } );
        } );
    }

    // ── Handle fix / dismiss (event delegation) ────────────────────────────
    async function handleResultAction( e ) {
        const btn = e.target.closest( '[data-action]' );
        if ( ! btn ) return;

        if ( btn.dataset.action === 'post-fix-all' ) return;

        // Undo / revert a previously applied fix.
        if ( btn.dataset.action === 'revert' ) {
            await handleRevertFix( btn );
            return;
        }

        // Restore a dismissed result back to open.
        if ( btn.dataset.action === 'restore' ) {
            await handleRestoreDismissed( btn );
            return;
        }

        // Add word to dictionary and dismiss all matching results.
        if ( btn.dataset.action === 'add-to-dictionary' ) {
            await handleAddToDictionary( btn );
            return;
        }

        const resultId = btn.dataset.id;
        const action   = btn.dataset.action;
        const accepted = action === 'fix';

        // ── Backup warning (shown once, before the very first fix) ──────
        if ( accepted && ! backupDismissed ) {
            var proceed = await showBackupWarning();
            if ( ! proceed ) return;          // user cancelled
            backupDismissed = true;
            // Persist so it never shows again.
            apiFetch( restUrl + '/backup-dismissed', { method: 'POST' } ).catch( function() {} );
        }

        // Read the current value from the editable input in the same row.
        // Scan-page rows use .spellmatic-result-row[data-id]; Results-page rows are <tr data-result-id>.
        const row           = document.querySelector( '.spellmatic-result-row[data-id="' + resultId + '"]' )
                           || btn.closest( 'tr[data-result-id]' );
        const fixInput      = row ? row.querySelector( '.spellmatic-fix-input' ) : null;
        const customCorrection = fixInput ? fixInput.value.trim() : '';

        btn.disabled    = true;
        btn.textContent = accepted ? 'Fixing…' : 'Dismissing…';

        try {
            const payload = {
                result_id:   parseInt( resultId, 10 ),
                accepted:    accepted,
            };
            // Pass the edited correction (may differ from the stored suggestion).
            if ( accepted && customCorrection ) {
                payload.custom_suggestion = customCorrection;
            }

            const fixResponse = await apiFetch( restUrl + '/fix', {
                method:  'POST',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify( payload ),
            } );

            if ( row ) {
                row.classList.add( 'spellmatic-resolved' );

                if ( accepted ) {
                    // Preserve the original word + suggestion so Undo can restore them.
                    var prevInput = row.querySelector( '.spellmatic-fix-input' );
                    if ( prevInput ) {
                        row.dataset.suggestion = prevInput.value || prevInput.placeholder || '';
                    }
                    var prevOrig = row.querySelector( '.spellmatic-original-word' );
                    if ( prevOrig ) {
                        row.dataset.original = prevOrig.textContent.trim();
                    }

                    // Replace action buttons with an inline Undo button.
                    // Works in all layouts: scan-page (.spellmatic-fix-row),
                    // results-page grouped cards (.spellmatic-fix-row),
                    // legacy table (td:last-child), flat rows (.spellmatic-result-actions).
                    var actionsCell = row.querySelector( '.spellmatic-fix-row' )
                                  || row.querySelector( '.spellmatic-result-actions' )
                                  || row.querySelector( 'td:last-child' );
                    if ( actionsCell ) {
                        actionsCell.innerHTML =
                            '<span class="spellmatic-fixed-label">&#10003; Fixed</span> ' +
                            '<button class="button button-small spellmatic-undo-btn" data-id="' + resultId + '" data-action="revert">Undo</button>';
                    }
                    row.classList.add( 'spellmatic-result-row--fixed' );
                    // Show a toast notification with undo option.
                    showUndoToast( resultId );
                } else {
                    // Dismiss — fade out after a short delay.
                    setTimeout( function() {
                        var group = row.closest( '.spellmatic-post-group' );
                        row.remove();
                        updateGroupCount( group );
                        updateResultCount();
                    }, 300 );
                }
            }
        } catch ( err ) {
            btn.disabled    = false;
            btn.textContent = accepted ? 'Apply Fix' : 'Dismiss';

            showError( err.message || 'Could not apply fix.' );
        }
    }

    // ── Add word to dictionary and dismiss all matching results ─────────────
    async function handleAddToDictionary( btn ) {
        var word = ( btn.dataset.word || '' ).trim();
        if ( ! word ) return;

        btn.disabled    = true;
        btn.textContent = 'Adding…';

        try {
            var data = await apiFetch( restUrl + '/dictionary/add', {
                method:  'POST',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify( { word: word } ),
            } );

            // Purge matching results from ALL in-memory group arrays (not just visible DOM)
            // so pagination re-renders correctly on every page.
            var lowerWord = word.toLowerCase();

            function purgeFromGroups( groups ) {
                if ( ! groups ) return;
                for ( var i = groups.length - 1; i >= 0; i-- ) {
                    groups[ i ].results = groups[ i ].results.filter( function( r ) {
                        return ( r.original || '' ).toLowerCase() !== lowerWord;
                    } );
                    // Remove the entire group if no results remain.
                    if ( groups[ i ].results.length === 0 ) {
                        groups.splice( i, 1 );
                    }
                }
            }

            // Scan-page data
            purgeFromGroups( allPostGroups );
            purgeFromGroups( filteredGroups );

            // Results-page data
            purgeFromGroups( _rpAllGroups );
            purgeFromGroups( _rpFilteredGroups );

            // Re-render the currently visible page (scan page or results page).
            if ( typeof renderCurrentPage === 'function' && resultsList ) {
                renderCurrentPage();
            }
            if ( typeof _rpRenderPage === 'function' && _rpRenderPage ) {
                _rpRenderPage();
            }

            updateResultCount();

            // Show a brief toast confirming the action.
            var toast = document.createElement( 'div' );
            toast.className = 'spellmatic-toast';
            toast.textContent = '\u201C' + word + '\u201D added to dictionary' +
                ( data.dismissed ? ' — ' + data.dismissed + ' error' + ( data.dismissed !== 1 ? 's' : '' ) + ' dismissed' : '' );
            document.body.appendChild( toast );
            setTimeout( function() { toast.remove(); }, 4000 );
        } catch ( err ) {
            btn.disabled    = false;
            btn.textContent = 'Dictionary';
            showError( err.message );
        }
    }

    // ── Undo / revert a fix ─────────────────────────────────────────────────
    async function handleRevertFix( btn ) {
        const resultId = parseInt( btn.dataset.id, 10 );
        if ( ! resultId ) return;

        btn.disabled    = true;
        btn.textContent = 'Reverting…';

        try {
            await apiFetch( restUrl + '/fix/revert', {
                method:  'POST',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify( { result_id: resultId } ),
            } );

            // Dismiss any visible undo toast for this result.
            var toast = document.getElementById( 'spellmatic-undo-toast' );
            if ( toast ) toast.remove();

            // ── Restore the row back to "open" state ──

            // Card-layout row (.spellmatic-result-row) — works for scan page,
            // Results page grouped cards, and AI grouped cards.
            var cardRow = document.querySelector( '.spellmatic-result-row[data-id="' + resultId + '"]' );
            if ( cardRow ) {
                cardRow.classList.remove( 'spellmatic-resolved', 'spellmatic-fixed-row', 'spellmatic-result-row--fixed' );

                // The fix-row was replaced with "Fixed + Undo" — rebuild the
                // original fix row with input + Apply Fix + Dismiss buttons.
                var fixRow = cardRow.querySelector( '.spellmatic-fix-row' );
                if ( fixRow ) {
                    var origEl     = cardRow.querySelector( '.spellmatic-original-word' );
                    var origWord   = ( origEl ? origEl.textContent.trim() : '' ) || cardRow.dataset.original || '';
                    var suggestion = cardRow.dataset.suggestion || '';
                    fixRow.innerHTML =
                        '<span class="spellmatic-original-word">' + escHtml( origWord ) + '</span>' +
                        '<span class="spellmatic-arrow-icon">&#8594;</span>' +
                        '<input type="text" class="spellmatic-fix-input" ' +
                            'value="' + escAttr( suggestion ) + '" ' +
                            'placeholder="' + ( suggestion ? escAttr( suggestion ) : 'type correction…' ) + '" ' +
                            'data-original="' + escAttr( origWord ) + '" ' +
                            'aria-label="Correction for ' + escAttr( origWord ) + '" />' +
                        '<button class="button button-primary spellmatic-fix-btn" ' +
                            'data-id="' + resultId + '" data-action="fix">Apply Fix</button>' +
                        '<button class="button spellmatic-dismiss-btn" ' +
                            'data-id="' + resultId + '" data-action="dismiss">Dismiss</button>' +
                        '<button class="button spellmatic-dict-add-inline" ' +
                            'data-word="' + escAttr( origWord ) + '" data-action="add-to-dictionary" ' +
                            'title="Add to dictionary and dismiss all occurrences">' +
                            '<span class="dashicons dashicons-book" style="font-size:14px;line-height:1.8;margin-right:2px;"></span>Dictionary</button>';
                }

                // Remove any Fixed status badge that was shown.
                var fixedBadge = cardRow.querySelector( '.spellmatic-status-fixed' );
                if ( fixedBadge ) fixedBadge.remove();

                // Update the group count.
                var group = cardRow.closest( '.spellmatic-post-group' );
                updateGroupCount( group );
            }

            // Legacy Results-page row (<tr>) — kept for backward compatibility.
            var tableRow = document.querySelector( 'tr[data-result-id="' + resultId + '"]' );
            if ( tableRow ) {
                tableRow.classList.remove( 'spellmatic-resolved', 'spellmatic-fixed-row' );
                var statusCell = tableRow.querySelector( '.spellmatic-status-fixed' );
                if ( statusCell ) {
                    statusCell.className = 'spellmatic-status-open';
                    statusCell.textContent = 'Open';
                }
                var actionsCell = tableRow.querySelector( 'td:last-child' );
                if ( actionsCell ) {
                    actionsCell.innerHTML =
                        '<button class="button button-small spellmatic-fix-btn" data-id="' + resultId + '" data-action="fix">Fix</button> ' +
                        '<button class="button button-small spellmatic-dismiss-btn" data-id="' + resultId + '" data-action="dismiss">Dismiss</button>';
                }
            }

            // If neither row was found (e.g. toast on a different page), reload.
            if ( ! cardRow && ! tableRow ) {
                window.location.reload();
            }
        } catch ( err ) {
            btn.disabled    = false;
            btn.textContent = 'Undo';

            // 409 = the corrected word was not found in the current content
            // (user manually edited the post since the fix was applied).
            if ( err.statusCode === 409 ) {
                showInfoToast( 'This fix can\u2019t be undone because the content has been edited since the fix was applied.' );
                // Disable the Undo button so they don't keep trying.
                btn.disabled    = true;
                btn.textContent = 'Edited';
                btn.title       = 'The post has been manually edited — undo is no longer available.';
            } else {
                showError( err.message || 'Could not revert the fix.' );
            }
        }
    }

    // ── Restore a dismissed result back to open ────────────────────────────
    async function handleRestoreDismissed( btn ) {
        const resultId = parseInt( btn.dataset.id, 10 );
        if ( ! resultId ) return;

        btn.disabled    = true;
        btn.textContent = 'Restoring…';

        try {
            await apiFetch( restUrl + '/fix/restore', {
                method:  'POST',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify( { result_id: resultId } ),
            } );

            // Update the table row back to open state (scan page).
            var tableRow = document.querySelector( 'tr[data-result-id="' + resultId + '"]' );
            if ( tableRow ) {
                tableRow.classList.remove( 'spellmatic-dismissed-row' );
                var statusCell = tableRow.querySelector( '.spellmatic-status-dismissed' );
                if ( statusCell ) {
                    statusCell.className = 'spellmatic-status-open';
                    statusCell.textContent = 'Open';
                }
                // Remove strikethrough from the error column.
                var errorCode = tableRow.querySelector( '.spellmatic-error' );
                if ( errorCode ) {
                    errorCode.style.textDecoration = '';
                    errorCode.style.opacity = '';
                }
                var actionsCell = tableRow.querySelector( 'td:last-child' );
                if ( actionsCell ) {
                    actionsCell.innerHTML =
                        '<button class="button button-small spellmatic-fix-btn" data-id="' + resultId + '" data-action="fix">Fix</button> ' +
                        '<button class="button button-small spellmatic-dismiss-btn" data-id="' + resultId + '" data-action="dismiss">Dismiss</button>';
                }
            }

            // Update the Results-page div row back to open state.
            var rpRow = document.querySelector( '.spellmatic-result-row[data-id="' + resultId + '"]' );
            if ( rpRow ) {
                // Update the in-memory data so filters stay correct.
                [ _rpAllGroups, _rpFilteredGroups ].forEach( function( groups ) {
                    if ( ! groups ) return;
                    groups.forEach( function( g ) {
                        g.results.forEach( function( r ) {
                            if ( String( r.id ) === String( resultId ) ) r.status = 'open';
                        } );
                    } );
                } );
                // Re-render current page to reflect the change.
                if ( typeof _rpRenderPage === 'function' ) {
                    _rpRenderPage();
                } else {
                    window.location.reload();
                }
            }

            if ( ! tableRow && ! rpRow ) {
                window.location.reload();
            }
        } catch ( err ) {
            btn.disabled    = false;
            btn.textContent = 'Restore';
            showError( err.message || 'Could not restore the dismissed result.' );
        }
    }

    // ── Backup warning modal (shown once before first fix) ─────────────────────
    function showBackupWarning() {
        return new Promise( function( resolve ) {
            // Build overlay + modal.
            var overlay = document.createElement( 'div' );
            overlay.className = 'spellmatic-backup-overlay';

            var modal = document.createElement( 'div' );
            modal.className = 'spellmatic-backup-modal';
            modal.innerHTML =
                '<div class="spellmatic-backup-icon">' +
                    '<span class="dashicons dashicons-warning" style="font-size:36px;width:36px;height:36px;color:#f59e0b;"></span>' +
                '</div>' +
                '<h3 style="margin:0 0 8px;">Back up your site first</h3>' +
                '<p style="margin:0 0 16px;color:#64748b;font-size:13px;line-height:1.5;">' +
                    'Spellmatic edits your posts and pages directly. ' +
                    'We strongly recommend creating a <strong>full site backup</strong> before applying fixes, ' +
                    'so you can restore your content if anything goes wrong.' +
                '</p>' +
                '<div style="display:flex;gap:8px;justify-content:flex-end;">' +
                    '<button class="button spellmatic-backup-cancel">Cancel</button>' +
                    '<button class="button button-primary spellmatic-backup-proceed">I have a backup — continue</button>' +
                '</div>';

            overlay.appendChild( modal );
            document.body.appendChild( overlay );

            overlay.querySelector( '.spellmatic-backup-cancel' ).addEventListener( 'click', function() {
                overlay.remove();
                resolve( false );
            } );
            overlay.querySelector( '.spellmatic-backup-proceed' ).addEventListener( 'click', function() {
                overlay.remove();
                resolve( true );
            } );
        } );
    }

    // ── Info toast (non-error, informational message) ─────────────────────────
    function showInfoToast( message ) {
        var existing = document.getElementById( 'spellmatic-info-toast' );
        if ( existing ) existing.remove();

        var toast = document.createElement( 'div' );
        toast.id        = 'spellmatic-info-toast';
        toast.className = 'spellmatic-info-toast';
        toast.innerHTML =
            '<span class="dashicons dashicons-info" style="margin-right:6px;"></span>' +
            '<span>' + message + '</span> ' +
            '<button class="spellmatic-info-toast-dismiss" type="button" title="Dismiss">&times;</button>';
        document.body.appendChild( toast );

        toast.querySelector( '.spellmatic-info-toast-dismiss' ).addEventListener( 'click', function() {
            toast.remove();
        } );

        // Auto-dismiss after 8 seconds for info messages.
        setTimeout( function() { if ( toast.parentNode ) toast.remove(); }, 8000 );
    }

    // ── Undo toast notification ───────────────────────────────────────────────
    function showUndoToast( resultId ) {
        // Remove any existing toast.
        var existing = document.getElementById( 'spellmatic-undo-toast' );
        if ( existing ) existing.remove();

        var toast = document.createElement( 'div' );
        toast.id = 'spellmatic-undo-toast';
        toast.className = 'spellmatic-undo-toast';
        toast.innerHTML =
            '<span>Fix applied.</span> ' +
            '<button class="button button-small spellmatic-undo-toast-btn" data-id="' + resultId + '" data-action="revert">Undo</button>' +
            '<button class="spellmatic-undo-toast-dismiss" type="button" title="Dismiss">&times;</button>';
        document.body.appendChild( toast );

        // Wire up the undo button in the toast.
        toast.querySelector( '.spellmatic-undo-toast-btn' ).addEventListener( 'click', function( e ) {
            handleRevertFix( e.target );
        } );

        // Dismiss (X) button just removes the toast — undo stays available in the row.
        toast.querySelector( '.spellmatic-undo-toast-dismiss' ).addEventListener( 'click', function() {
            toast.remove();
        } );
    }

    // ── Results page: Bulk Dismiss All ──────────────────────────────────────
    async function handleResultsPageBulkDismiss( e ) {
        const btn  = e.currentTarget;
        const rows = Array.from( document.querySelectorAll(
            '#spellmatic-results-page-container .spellmatic-result-row[data-id], ' +
            '#spellmatic-results-table-wrap tr[data-result-id]'
        ) );
        if ( ! rows.length ) return;

        if ( ! confirm( 'Dismiss all ' + rows.length + ' error(s) on this page?' ) ) return;

        btn.disabled    = true;
        btn.textContent = 'Dismissing…';

        let done = 0;
        for ( const row of rows ) {
            const resultId = parseInt( row.dataset.id || row.dataset.resultId, 10 );
            if ( ! resultId ) continue;
            if ( row.classList.contains( 'spellmatic-result-row--fixed' ) ) continue;
            try {
                await apiFetch( restUrl + '/fix', {
                    method:  'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body:    JSON.stringify( { result_id: resultId, accepted: false } ),
                } );
                row.classList.add( 'spellmatic-resolved' );
                setTimeout( function() { row.remove(); }, 300 );
                done++;
            } catch ( err ) {
                // Continue with remaining rows even if one fails.
            }
        }

        btn.disabled    = false;
        btn.textContent = 'Dismiss All';

        if ( done > 0 ) {
            setTimeout( function() { window.location.reload(); }, 500 );
        }
    }

    // ── Update the error count badge on a post group after fixes ─────────────
    function updateGroupCount( group ) {
        if ( ! group ) return;
        const remaining = group.querySelectorAll( '.spellmatic-result-row' ).length;
        const countEl   = group.querySelector( '.spellmatic-group-count' );

        if ( remaining === 0 ) {
            group.classList.add( 'spellmatic-resolved' );
            setTimeout( function() { group.remove(); }, 400 );
            return;
        }
        if ( countEl ) countEl.textContent = remaining + ' error' + ( remaining !== 1 ? 's' : '' );
    }

    // ── Wait for Harper bridge to be ready ────────────────────────────────────
    // harper-bridge.js (ES module) sets window.__spellmaticHarper immediately
    // on load and begins initialising. We poll for the global, then await ready.
    async function waitForHarper( timeoutMs ) {
        timeoutMs = timeoutMs || 60000;
        const deadline = Date.now() + timeoutMs;

        while ( ! window.__spellmaticHarper ) {
            if ( Date.now() > deadline ) {
                throw new Error( 'Harper grammar engine did not load within ' + ( timeoutMs / 1000 ) + 's. Check your browser console.' );
            }
            await sleep( 200 );
        }

        return await window.__spellmaticHarper.ready;
    }

    // ── Helpers ───────────────────────────────────────────────────────────────
    function showProgress( pct, text ) {
        if ( progressWrap ) progressWrap.classList.remove( 'hidden' );
        if ( progressFill ) progressFill.style.width = pct + '%';
        if ( progressText ) progressText.textContent  = text;
    }

    function showNoErrors() {
        if ( resultsContainer ) resultsContainer.classList.remove( 'hidden' );
        if ( resultsTitle )     resultsTitle.textContent = strings.noErrors || 'No errors found.';
        if ( resultsList )      resultsList.innerHTML    = '<p class="spellmatic-no-errors">&#10003; ' + ( strings.noErrors || 'Great news — no errors found!' ) + '</p>';
    }

    function updateResultCount() {
        if ( ! resultsList || ! resultsTitle ) return;
        // Count across all rendered groups + any unrendered pages.
        const rendered   = resultsList.querySelectorAll( '.spellmatic-result-row' ).length;
        const unrendered = filteredGroups
            .filter( function(g) {
                return ! resultsList.querySelector( '.spellmatic-post-group[data-post-id="' + g.postId + '"]' );
            } )
            .reduce( function(sum, g) { return sum + g.results.length; }, 0 );
        const remaining  = rendered + unrendered;
        resultsTitle.textContent = remaining + ' ' + ( remaining === 1
            ? ( strings.errorFound  || 'error found' )
            : ( strings.errorsFound || 'errors found' ) );
        if ( remaining === 0 ) showNoErrors();
    }

    function showError( msg ) {
        clearError();
        const el = document.createElement( 'div' );
        el.className = 'notice notice-error is-dismissible spellmatic-error-notice';
        el.innerHTML = '<p><strong>Spellmatic error:</strong> ' + escHtml( msg ) + '</p>' +
                       '<p style="font-size:12px;color:#777;">Open DevTools (F12) → Console for more details.</p>';

        const wrap = document.querySelector( '.spellmatic-wrap' );
        if ( wrap ) {
            wrap.insertBefore( el, wrap.firstChild );
        } else {
            document.body.insertBefore( el, document.body.firstChild );
        }
    }

    function clearError() {
        const existing = document.querySelector( '.spellmatic-error-notice' );
        if ( existing ) existing.remove();
    }

    function resetScanBtn() {
        if ( ! scanBtn ) return;
        scanBtn.disabled = false;
        updateScanBtnLabel();
    }

    function sleep( ms ) {
        return new Promise( function( resolve ) { setTimeout( resolve, ms ); } );
    }

    /**
     * Human-readable label for the `field` column.
     *   post_title          → "title"
     *   post_content        → "content"
     *   elementor           → "Elementor" (legacy combined)
     *   elementor_heading   → "Heading widget"
     *   elementor_text-editor → "Text Editor widget"
     *   elementor_button    → "Button widget"
     */
    function formatFieldLabel( field ) {
        if ( field === 'post_title' )   return 'title';
        if ( field === 'post_content' ) return 'content';
        if ( field === 'elementor' )    return 'Elementor';
        if ( field.indexOf( 'elementor_' ) === 0 ) {
            const widgetType = field.slice( 'elementor_'.length ); // e.g. "text-editor"
            const pretty = widgetType
                .split( /[-_]/ )
                .map( function( w ) { return ucfirst( w ); } )
                .join( ' ' ); // "Text Editor"
            return pretty + ' widget';
        }
        return field.replace( 'post_', '' );
    }

    function escHtml( str ) {
        const div = document.createElement( 'div' );
        div.appendChild( document.createTextNode( String( str || '' ) ) );
        return div.innerHTML;
    }

    function escAttr( str ) {
        return String( str || '' ).replace( /"/g, '&quot;' ).replace( /'/g, '&#39;' );
    }

    function escRegex( str ) {
        return String( str || '' ).replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
    }

    function ucfirst( str ) {
        return str ? str.charAt( 0 ).toUpperCase() + str.slice( 1 ) : '';
    }

    async function apiFetch( url, options ) {
        options = options || {};
        const headers = Object.assign( { 'X-WP-Nonce': nonce }, options.headers || {} );

        const response = await fetch( url, Object.assign( {}, options, { headers: headers } ) );

        if ( ! response.ok ) {
            let errBody = {};
            try { errBody = await response.json(); } catch( e ) {}
            const err = new Error( errBody.message || errBody.error || ( 'HTTP ' + response.status + ' — ' + response.statusText ) );
            err.statusCode = response.status;
            err.errorCode  = errBody.error  || null;
            err.errorData  = errBody;
            throw err;
        }

        return response.json();
    }

    // =========================================================================
    // Phase 4 — Custom Dictionary panel
    // =========================================================================

    function initDictionaryPanel() {
        const addBtn    = document.getElementById( 'spellmatic-dict-add-btn' );
        const input     = document.getElementById( 'spellmatic-dict-input' );
        const statusEl  = document.getElementById( 'spellmatic-dict-status' );

        // Render the initial word list from the localized data.
        const initialWords = ( window.spellmatic.dictionary || [] ).slice();
        renderDictList( initialWords );

        if ( addBtn ) {
            addBtn.addEventListener( 'click', function() {
                handleDictAdd( input, statusEl );
            } );
        }

        if ( input ) {
            input.addEventListener( 'keydown', function( e ) {
                if ( e.key === 'Enter' ) {
                    e.preventDefault();
                    handleDictAdd( input, statusEl );
                }
            } );
        }

        // Event delegation for remove buttons.
        const listEl = document.getElementById( 'spellmatic-dict-list' );
        if ( listEl ) {
            listEl.addEventListener( 'click', function( e ) {
                const btn = e.target.closest( '.spellmatic-dict-remove' );
                if ( ! btn ) return;
                handleDictRemove( btn.dataset.word, statusEl );
            } );
        }
    }

    async function handleDictAdd( input, statusEl ) {
        if ( ! input ) return;
        const word = input.value.trim();
        if ( ! word ) return;

        input.disabled = true;
        setStatus( statusEl, 'Saving…', '' );

        try {
            const data = await apiFetch( restUrl + '/dictionary/add', {
                method:  'POST',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify( { word: word } ),
            } );
            renderDictList( data.words || [] );
            input.value = '';
            const added = data.added || 0;
            setStatus( statusEl, added > 0 ? '✓ Added' : 'Already in dictionary', 'success' );
            setTimeout( function() { setStatus( statusEl, '', '' ); }, 2500 );
        } catch( err ) {
            setStatus( statusEl, 'Error: ' + err.message, 'error' );
        } finally {
            input.disabled = false;
            input.focus();
        }
    }

    async function handleDictRemove( word, statusEl ) {
        if ( ! word ) return;
        setStatus( statusEl, 'Removing…', '' );

        try {
            const data = await apiFetch( restUrl + '/dictionary/remove', {
                method:  'POST',
                headers: { 'Content-Type': 'application/json' },
                body:    JSON.stringify( { word: word } ),
            } );
            renderDictList( data.words || [] );
            setStatus( statusEl, '✓ Removed', 'success' );
            setTimeout( function() { setStatus( statusEl, '', '' ); }, 2000 );
        } catch( err ) {
            setStatus( statusEl, 'Error: ' + err.message, 'error' );
        }
    }

    /**
     * Re-render the dictionary tag list.
     * @param {string[]} words
     */
    function renderDictList( words ) {
        const listEl = document.getElementById( 'spellmatic-dict-list' );
        const emptyEl = listEl ? listEl.querySelector( '.spellmatic-dict-empty' ) : null;
        if ( ! listEl ) return;

        // Remove existing tags (leave the empty-state paragraph).
        Array.from( listEl.querySelectorAll( '.spellmatic-dict-tag' ) ).forEach( function( el ) {
            el.remove();
        } );

        if ( words.length === 0 ) {
            if ( emptyEl ) emptyEl.classList.remove( 'hidden' );
            return;
        }

        if ( emptyEl ) emptyEl.classList.add( 'hidden' );

        words.forEach( function( word ) {
            const tag = document.createElement( 'span' );
            tag.className = 'spellmatic-dict-tag';
            tag.innerHTML =
                '<span class="spellmatic-dict-word">' + escHtml( word ) + '</span>' +
                '<button class="spellmatic-dict-remove" data-word="' + escAttr( word ) + '" ' +
                    'title="Remove \'' + escAttr( word ) + '\' from dictionary" ' +
                    'aria-label="Remove ' + escAttr( word ) + '">' +
                    '&times;' +
                '</button>';
            listEl.appendChild( tag );
        } );
    }

    // =========================================================================
    // Phase 4 — Dictionary Export / Import
    // =========================================================================

    function initDictExportImport() {
        var exportBtn    = document.getElementById( 'spellmatic-dict-export-btn' );
        var importBtn    = document.getElementById( 'spellmatic-dict-import-btn' );
        var importPanel  = document.getElementById( 'spellmatic-dict-import-panel' );
        var importInput  = document.getElementById( 'spellmatic-dict-import-input' );
        var importSave   = document.getElementById( 'spellmatic-dict-import-save-btn' );
        var importCancel = document.getElementById( 'spellmatic-dict-import-cancel-btn' );
        var statusEl     = document.getElementById( 'spellmatic-dict-export-status' );

        if ( exportBtn ) {
            exportBtn.addEventListener( 'click', function() {
                var tags  = document.querySelectorAll( '#spellmatic-dict-list .spellmatic-dict-word' );
                var words = Array.from( tags ).map( function( el ) { return el.textContent.trim(); } );
                if ( words.length === 0 ) {
                    setStatus( statusEl, 'Dictionary is empty — nothing to export.', 'error' );
                    setTimeout( function() { setStatus( statusEl, '', '' ); }, 3000 );
                    return;
                }
                var csv  = words.join( ', ' );
                var blob = new Blob( [ csv ], { type: 'text/csv' } );
                var url  = URL.createObjectURL( blob );
                var a    = document.createElement( 'a' );
                a.href     = url;
                a.download = 'spellmatic-dictionary.csv';
                document.body.appendChild( a );
                a.click();
                document.body.removeChild( a );
                URL.revokeObjectURL( url );
                setStatus( statusEl, '✓ Exported ' + words.length + ' word' + ( words.length === 1 ? '' : 's' ), 'success' );
                setTimeout( function() { setStatus( statusEl, '', '' ); }, 3000 );
            } );
        }

        if ( importBtn && importPanel ) {
            importBtn.addEventListener( 'click', function() {
                importPanel.classList.toggle( 'hidden' );
                if ( ! importPanel.classList.contains( 'hidden' ) && importInput ) {
                    importInput.focus();
                }
            } );
        }

        if ( importCancel && importPanel ) {
            importCancel.addEventListener( 'click', function() {
                importPanel.classList.add( 'hidden' );
                if ( importInput ) importInput.value = '';
            } );
        }

        if ( importSave ) {
            importSave.addEventListener( 'click', async function() {
                if ( ! importInput ) return;
                var raw = importInput.value.trim();
                if ( ! raw ) return;

                importSave.disabled = true;
                setStatus( statusEl, 'Importing…', '' );

                try {
                    var data = await apiFetch( restUrl + '/dictionary/add', {
                        method:  'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body:    JSON.stringify( { word: raw } ),
                    } );
                    renderDictList( data.words || [] );
                    var added = data.added || 0;
                    importInput.value = '';
                    importPanel.classList.add( 'hidden' );
                    setStatus( statusEl, '✓ Imported ' + added + ' word' + ( added === 1 ? '' : 's' ), 'success' );
                    setTimeout( function() { setStatus( statusEl, '', '' ); }, 3000 );
                } catch( err ) {
                    setStatus( statusEl, 'Error: ' + err.message, 'error' );
                } finally {
                    importSave.disabled = false;
                }
            } );
        }
    }

    // =========================================================================
    // Phase 4 — Scan Digest panel
    // =========================================================================

    function initDigestPanel() {
        const enabledEl   = document.getElementById( 'spellmatic-digest-enabled' );
        const freqEl      = document.getElementById( 'spellmatic-digest-frequency' );
        const emailEl     = document.getElementById( 'spellmatic-digest-email' );
        const nextEl      = document.getElementById( 'spellmatic-digest-next' );
        const saveBtn     = document.getElementById( 'spellmatic-digest-save-btn' );
        const testBtn     = document.getElementById( 'spellmatic-digest-test-btn' );
        const statusEl    = document.getElementById( 'spellmatic-digest-status' );

        // Populate from localized settings.
        if ( enabledEl ) enabledEl.checked = !! digestConfig.enabled;
        if ( freqEl    ) freqEl.value      = digestConfig.frequency || 'weekly';
        if ( emailEl   ) emailEl.value     = digestConfig.email     || '';
        if ( nextEl    ) nextEl.textContent = digestConfig.nextScheduled
            ? 'Next: ' + digestConfig.nextScheduled
            : ( digestConfig.enabled ? 'Pending…' : 'Disabled' );

        if ( saveBtn ) {
            saveBtn.addEventListener( 'click', async function() {
                saveBtn.disabled = true;
                setStatus( statusEl, 'Saving…', '' );
                try {
                    const data = await apiFetch( restUrl + '/digest/settings', {
                        method:  'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body:    JSON.stringify( {
                            enabled:   enabledEl ? enabledEl.checked : false,
                            frequency: freqEl    ? freqEl.value      : 'weekly',
                            email:     emailEl   ? emailEl.value     : '',
                        } ),
                    } );
                    if ( nextEl ) {
                        nextEl.textContent = data.next_scheduled
                            ? 'Next: ' + data.next_scheduled
                            : ( data.digest_enabled ? 'Pending…' : 'Disabled' );
                    }
                    setStatus( statusEl, '✓ Settings saved', 'success' );
                    setTimeout( function() { setStatus( statusEl, '', '' ); }, 2500 );
                } catch( err ) {
                    setStatus( statusEl, 'Error: ' + err.message, 'error' );
                } finally {
                    saveBtn.disabled = false;
                }
            } );
        }

        if ( testBtn ) {
            testBtn.addEventListener( 'click', async function() {
                testBtn.disabled    = true;
                testBtn.textContent = 'Sending…';
                setStatus( statusEl, '', '' );
                try {
                    const data = await apiFetch( restUrl + '/digest/test', { method: 'POST' } );
                    setStatus( statusEl, '✓ Test email sent to ' + escHtml( data.to || '' ), 'success' );
                    setTimeout( function() { setStatus( statusEl, '', '' ); }, 4000 );
                } catch( err ) {
                    setStatus( statusEl, 'Error: ' + err.message, 'error' );
                } finally {
                    testBtn.disabled    = false;
                    testBtn.textContent = 'Send Test Email Now';
                }
            } );
        }
    }

    // =========================================================================
    // Phase 5 — BYOK provider test buttons (settings page)
    // =========================================================================

    function initByokTestButtons() {
        const btns = document.querySelectorAll( '.spellmatic-byok-test-btn' );

        // Enable button when user types or pastes into the API key input.
        // If a key is already stored the button starts enabled and stays that way.
        btns.forEach( function( btn ) {
            const provider    = btn.dataset.provider;
            const input       = document.querySelector( 'input[name="byok_' + provider + '"]' );
            const alreadyHas  = ! btn.disabled;          // true if a key is already stored
            if ( input ) {
                input.addEventListener( 'input', function() {
                    btn.disabled = ! alreadyHas && ! input.value.trim();
                } );
            }
        } );

        // Click handler: save the key first (if a new one was entered), then test.
        btns.forEach( function( btn ) {
            btn.addEventListener( 'click', async function() {
                const provider  = btn.dataset.provider;
                const input     = document.querySelector( 'input[name="byok_' + provider + '"]' );
                const statusEl  = document.querySelector(
                    '.spellmatic-byok-test-status[data-provider="' + provider + '"]'
                );
                const newKey    = input ? input.value.trim() : '';

                btn.disabled    = true;
                btn.textContent = 'Saving…';
                if ( statusEl ) setStatus( statusEl, '', '' );

                try {
                    // If a new key was entered, save it first.
                    if ( newKey ) {
                        btn.textContent = 'Saving…';
                        const saveData = await apiFetch( restUrl + '/byok/save', {
                            method:  'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body:    JSON.stringify( { provider: provider, key: newKey } ),
                        } );

                        if ( ! saveData.saved ) {
                            if ( statusEl ) setStatus( statusEl, '✗ Failed to save key.', 'error' );
                            return;
                        }

                        // Clear the input and show the connected indicator.
                        input.value       = '';
                        input.placeholder = 'Enter new key to replace…';
                    }

                    // Now test the (stored) key.
                    btn.textContent = 'Testing…';
                    const data = await apiFetch( restUrl + '/byok/test', {
                        method:  'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body:    JSON.stringify( { provider: provider } ),
                    } );

                    if ( data.ok ) {
                        if ( statusEl ) setStatus( statusEl, '✓ ' + ( data.message || 'Connected' ), 'success' );
                    } else {
                        if ( statusEl ) setStatus( statusEl, '✗ ' + ( data.message || 'Failed' ), 'error' );
                    }
                } catch ( err ) {
                    if ( statusEl ) setStatus( statusEl, '✗ ' + ( err.message || 'Error' ), 'error' );
                } finally {
                    btn.disabled    = false;
                    btn.textContent = 'Test Connection';
                }
            } );
        } );
    }

    // =========================================================================
    // Phase 5 — AI toggle button (dashboard scan panel)
    // =========================================================================

    function initAiToggle() {
        const toggleBtn = document.getElementById( 'spellmatic-ai-toggle' );
        if ( ! toggleBtn ) return;

        toggleBtn.addEventListener( 'click', async function() {
            const newEnabled = ! byokConfig.aiEnabled;

            // Optimistically update UI.
            byokConfig.aiEnabled = newEnabled;
            toggleBtn.classList.toggle( 'ai-on',  newEnabled );
            toggleBtn.classList.toggle( 'ai-off', ! newEnabled );
            toggleBtn.setAttribute( 'aria-pressed', String( newEnabled ) );
            toggleBtn.querySelector( '.ai-toggle-label' ).textContent = 'AI ' + ( newEnabled ? 'ON' : 'OFF' );

            // Persist to server.
            try {
                await apiFetch( restUrl + '/byok/ai-settings', {
                    method:  'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body:    JSON.stringify( { ai_enabled: newEnabled } ),
                } );
            } catch ( err ) {
                // Revert on failure.
                byokConfig.aiEnabled = ! newEnabled;
                toggleBtn.classList.toggle( 'ai-on',  ! newEnabled );
                toggleBtn.classList.toggle( 'ai-off', newEnabled );
                toggleBtn.setAttribute( 'aria-pressed', String( ! newEnabled ) );
                toggleBtn.querySelector( '.ai-toggle-label' ).textContent = 'AI ' + ( ! newEnabled ? 'ON' : 'OFF' );
            }
        } );
    }

    // =========================================================================
    // Phase 5 — AI cost-estimate preview (settings page)
    // =========================================================================

    function initAiCostPreview() {
        const maxInput   = document.querySelector( 'input[name="ai_max_items"]' );
        const previewEl  = document.getElementById( 'spellmatic-ai-cost-preview' );
        if ( ! maxInput || ! previewEl ) return;

        const estimates = byokConfig.creditEstimates || {};
        const provider  = byokConfig.activeProvider  || 'openai';
        const est       = estimates[ provider ] || { per_item: '~$0.001', label: provider };

        function update() {
            const n    = Math.max( 1, parseInt( maxInput.value, 10 ) || 5 );
            const name = est.label || provider;
            previewEl.textContent = n + ' items × ' + est.per_item + '/item (' + name + ')';
        }

        maxInput.addEventListener( 'input', update );
        update();
    }

    // =========================================================================
    // Phase 5 — AI supplemental pass
    // =========================================================================

    /**
     * Run AI scan on items in batches of 5, sequentially.
     * Capped at AI_MAX_ITEMS total to control credit usage.
     */
    async function runAiPass( items ) {
        if ( ! scanId || ! items || items.length === 0 ) return;

        const provider     = byokConfig.activeProvider || '';
        const label        = providerLabel( provider );
        const BATCH        = 1;
        const AI_MAX_ITEMS = Math.max( 1, byokConfig.aiMaxItems || 5 );

        const aiItems = items.slice( 0, AI_MAX_ITEMS ).map( function( item ) {
            return {
                post_id:   item.post_id,
                post_type: item.post_type,
                field:     item.field,
                text:      item.text || '',
            };
        } );

        const batches = [];
        for ( let i = 0; i < aiItems.length; i += BATCH ) {
            batches.push( aiItems.slice( i, i + BATCH ) );
        }

        let totalFound   = 0;
        let firstWarning = '';

        for ( let b = 0; b < batches.length; b++ ) {
            const pct = Math.round( 90 + ( 10 * b / batches.length ) );
            showProgress( pct, label + ': checking batch ' + ( b + 1 ) + ' of ' + batches.length + '…' );

            try {
                const data = await apiFetch( restUrl + '/scan/ai', {
                    method:  'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body:    JSON.stringify( {
                        scan_id:  scanId,
                        items:    batches[ b ],
                        provider: provider,
                    } ),
                } );

                const aiErrors = data.errors || [];
                totalFound += aiErrors.length;
                if ( aiErrors.length > 0 ) appendAiResults( aiErrors );
                if ( data.warning && ! firstWarning ) firstWarning = data.warning;

            } catch ( err ) {
                console.warn( 'Spellmatic AI batch ' + ( b + 1 ) + ' error:', err );
                if ( ! firstWarning ) firstWarning = err.message || 'unknown error';
                break;
            }
        }

        if ( totalFound > 0 ) {
            showProgress( 100, label + ' found ' + totalFound + ' additional error' + ( totalFound !== 1 ? 's' : '' ) + '.' );
        } else if ( firstWarning ) {
            showProgress( 100, 'AI analysis skipped: ' + firstWarning );
        } else {
            showProgress( 100, label + ': no additional errors found.' );
        }
    }

    // ── Results page: grouped cards from server-provided data ──────────────────
    function initResultsPage() {
        var data        = window.spellmaticResults;
        if ( ! data || ! data.results ) return;

        var rpContainer = document.getElementById( 'spellmatic-results-page-container' );
        var rpList      = document.getElementById( 'spellmatic-rp-results-list' );
        var rpFilterBar = document.getElementById( 'spellmatic-rp-filter-bar' );
        var rpSearch    = document.getElementById( 'spellmatic-rp-filter-search' );
        var rpType      = document.getElementById( 'spellmatic-rp-filter-type' );
        var rpSort      = document.getElementById( 'spellmatic-rp-filter-sort' );
        var rpStatus    = document.getElementById( 'spellmatic-rp-filter-status' );
        var rpPagination= document.getElementById( 'spellmatic-rp-pagination' );

        if ( ! rpContainer || ! rpList ) return;

        var rpPostMeta  = data.postMeta || {};
        var allResults  = data.results  || [];

        if ( allResults.length === 0 ) {
            rpList.innerHTML = '<p>No errors found. Run a scan from the Dashboard.</p>';
            return;
        }

        // ── Group results by post_id ─────────────────────────────────────
        var rpGroupMap = {};
        allResults.forEach( function( r ) {
            var pid = String( r.post_id );
            if ( ! rpGroupMap[ pid ] ) {
                var meta = rpPostMeta[ pid ] || {};
                rpGroupMap[ pid ] = {
                    postId:    pid,
                    postTitle: meta.title || r.post_title || ( 'Post #' + pid ),
                    postType:  r.post_type || 'post',
                    pm:        {
                        permalink:        meta.permalink  || '',
                        builder_edit_url: '',
                        builder_edit_label: '',
                    },
                    results:   [],
                };
            }
            rpGroupMap[ pid ].results.push( r );
        } );

        var rpAllGroups     = Object.values( rpGroupMap );
        var rpFilteredGroups= rpAllGroups.slice();
        var rpCurrentPage   = 1;
        var RP_PAGE_SIZE    = 10;

        // Expose to module-level so handleAddToDictionary can purge across pages.
        _rpAllGroups      = rpAllGroups;
        _rpFilteredGroups = rpFilteredGroups;

        // ── Build error row for Results page (includes fixed state + undo) ─
        function buildRpErrorRow( r ) {
            var fieldLabel = formatFieldLabel( r.field || '' );
            var errorType  = r.error_type || 'spelling';
            var original   = r.original   || '';
            var suggestion = r.suggestion || '';
            var context    = r.context    || '';
            var isFixed    = ( r.status === 'fixed' );
            var isDismissed = ( r.status === 'dismissed' );

            var highlightedContext = context
                ? escHtml( context ).replace(
                    new RegExp( '(' + escRegex( original ) + ')', 'gi' ),
                    '<mark class="spellmatic-error-mark">$1</mark>'
                  )
                : '';

            var rowClass = isFixed ? ' spellmatic-result-row--fixed' : ( isDismissed ? ' spellmatic-result-row--dismissed' : '' );

            var actionsHtml = '';
            if ( isFixed ) {
                if ( r.history_id ) {
                    actionsHtml =
                        '<button class="button button-small spellmatic-undo-btn" ' +
                            'data-id="' + escAttr( r.id ) + '" data-action="revert">Undo</button>';
                }
            } else if ( isDismissed ) {
                actionsHtml =
                    '<button class="button button-small spellmatic-restore-btn" ' +
                        'data-id="' + escAttr( r.id ) + '" data-action="restore">Restore</button>';
            } else {
                actionsHtml =
                    '<button class="button button-primary spellmatic-fix-btn" ' +
                        'data-id="' + escAttr( r.id ) + '" data-action="fix">Apply Fix</button>' +
                    '<button class="button spellmatic-dismiss-btn" ' +
                        'data-id="' + escAttr( r.id ) + '" data-action="dismiss">Dismiss</button>' +
                    '<button class="button spellmatic-dict-add-inline" ' +
                        'data-word="' + escAttr( original ) + '" data-action="add-to-dictionary" ' +
                        'title="Add to dictionary and dismiss all occurrences">' +
                        '<span class="dashicons dashicons-book" style="font-size:14px;line-height:1.8;margin-right:2px;"></span>Dictionary</button>';
            }

            var statusBadge = isFixed
                ? '<span class="spellmatic-status-fixed">&#10003; Fixed</span>'
                : ( isDismissed
                    ? '<span class="spellmatic-status-dismissed">Dismissed</span>'
                    : '' );

            var originalDisplay = ( isFixed || isDismissed )
                ? '<span class="spellmatic-original-word" style="text-decoration:line-through;opacity:0.5;">' + escHtml( original ) + '</span>'
                : '<span class="spellmatic-original-word">' + escHtml( original ) + '</span>';

            return (
                '<div class="spellmatic-result-row' + rowClass + '" data-id="' + escAttr( r.id ) + '" data-suggestion="' + escAttr( suggestion ) + '" data-original="' + escAttr( original ) + '">' +

                    '<div class="spellmatic-result-meta">' +
                        '<span class="spellmatic-badge spellmatic-badge--' + escAttr( errorType ) + '">' +
                            ucfirst( errorType ) +
                        '</span>' +
                        ( isHighConfidence( r )
                            ? '<span class="spellmatic-badge spellmatic-badge--confidence-high" title="Spelling error with a close suggestion — safe to auto-fix">High</span>'
                            : '<span class="spellmatic-badge spellmatic-badge--confidence-low" title="Grammar, style, or ambiguous — review manually">Low</span>'
                        ) +
                        statusBadge +
                        '<span class="spellmatic-location">' + escHtml( fieldLabel ) + '</span>' +
                    '</div>' +

                    ( highlightedContext
                        ? '<div class="spellmatic-result-context">' +
                            '<span class="spellmatic-context-label">Context:</span> ' +
                            '&ldquo;' + highlightedContext + '&rdquo;' +
                          '</div>'
                        : '' ) +

                    '<div class="spellmatic-fix-row">' +
                        originalDisplay +
                        '<span class="spellmatic-arrow-icon">&#8594;</span>' +
                        ( ( isFixed || isDismissed )
                            ? '<span class="spellmatic-fix-applied">' + escHtml( suggestion ) + '</span>'
                            : '<input type="text" class="spellmatic-fix-input" ' +
                                'value="' + escAttr( suggestion ) + '" ' +
                                'placeholder="' + ( suggestion ? escAttr( suggestion ) : 'type correction…' ) + '" ' +
                                'data-original="' + escAttr( original ) + '" ' +
                                'aria-label="Correction for ' + escAttr( original ) + '" />'
                        ) +
                        actionsHtml +
                    '</div>' +

                '</div>'
            );
        }

        // ── Build one post-group card for Results page ───────────────────
        function buildRpGroupCard( group ) {
            var pid       = group.postId;
            var pm        = group.pm;
            var openCount = 0, fixedCount = 0, dismissedCount = 0;
            group.results.forEach( function( r ) {
                if ( r.status === 'open' ) openCount++;
                else if ( r.status === 'fixed' ) fixedCount++;
                else if ( r.status === 'dismissed' ) dismissedCount++;
            } );
            var totalCount = group.results.length;
            var typeLabel  = postTypes[ group.postType ] || ucfirst( group.postType );

            var editUrl  = escAttr( adminUrl + 'post.php?post=' + pid + '&action=edit' );
            var viewUrl  = pm.permalink ? escAttr( pm.permalink ) : '';

            var postLinks =
                ( viewUrl ? '<a href="' + viewUrl + '" target="_blank" class="spellmatic-post-link">View</a> <span class="spellmatic-link-sep">&middot;</span> ' : '' ) +
                '<a href="' + editUrl + '" target="_blank" class="spellmatic-post-link">Edit</a>';

            var errorRows = group.results.map( function( r ) {
                return buildRpErrorRow( r );
            } ).join( '' );

            var countLabel = openCount + ' open';
            if ( fixedCount > 0 ) {
                countLabel += ', ' + fixedCount + ' fixed';
            }
            if ( dismissedCount > 0 ) {
                countLabel += ', ' + dismissedCount + ' dismissed';
            }

            return (
                '<div class="spellmatic-post-group" data-post-id="' + escAttr( pid ) + '" ' +
                     'data-post-type="' + escAttr( group.postType ) + '" ' +
                     'data-error-count="' + openCount + '">' +

                    '<div class="spellmatic-post-group-header">' +
                        '<button class="spellmatic-group-toggle" aria-expanded="true" title="Collapse">' +
                            '<span class="spellmatic-toggle-icon">&#9660;</span>' +
                        '</button>' +
                        '<span class="spellmatic-type-badge spellmatic-type-badge--' + escAttr( group.postType ) + '">' +
                            escHtml( typeLabel ) +
                        '</span>' +
                        '<span class="spellmatic-group-title">' + escHtml( group.postTitle ) + '</span>' +
                        '<span class="spellmatic-group-count">' + countLabel + '</span>' +
                        '<span class="spellmatic-post-links">' + postLinks + '</span>' +
                    '</div>' +

                    '<div class="spellmatic-post-group-body">' +
                        errorRows +
                    '</div>' +

                '</div>'
            );
        }

        // ── Filter bar ───────────────────────────────────────────────────
        function initRpFilterBar() {
            if ( ! rpFilterBar ) return;

            // Populate type dropdown.
            if ( rpType ) {
                while ( rpType.options.length > 1 ) rpType.remove( 1 );
                var seenTypes = {};
                rpAllGroups.forEach( function(g) { seenTypes[ g.postType ] = true; } );
                Object.keys( seenTypes ).forEach( function( slug ) {
                    var opt = document.createElement( 'option' );
                    opt.value       = slug;
                    opt.textContent = postTypes[ slug ] || ucfirst( slug );
                    rpType.appendChild( opt );
                } );
            }

            // Wire up filter controls.
            [ rpSearch, rpType, rpSort, rpStatus ].forEach( function( el ) {
                if ( el ) el.addEventListener( 'input', rpApplyFiltersAndRender );
            } );

            rpFilterBar.classList.remove( 'hidden' );
        }

        function rpApplyFiltersAndRender() {
            var search = rpSearch ? rpSearch.value.trim().toLowerCase() : '';
            var type   = rpType   ? rpType.value   : '';
            var sort   = rpSort   ? rpSort.value   : 'errors-desc';
            var status = rpStatus ? rpStatus.value : '';

            rpFilteredGroups = rpAllGroups.filter( function( g ) {
                if ( type && g.postType !== type ) return false;
                if ( search && g.postTitle.toLowerCase().indexOf( search ) === -1 ) return false;
                if ( status ) {
                    // Check if group has any results matching the status filter.
                    var hasMatch = g.results.some( function( r ) { return r.status === status; } );
                    if ( ! hasMatch ) return false;
                }
                return true;
            } );

            // When filtering by status, also filter individual results within groups.
            if ( status ) {
                rpFilteredGroups = rpFilteredGroups.map( function( g ) {
                    return Object.assign( {}, g, {
                        results: g.results.filter( function( r ) { return r.status === status; } )
                    } );
                } );
            }

            if ( sort === 'errors-asc' ) {
                rpFilteredGroups.sort( function(a,b) { return a.results.length - b.results.length; } );
            } else if ( sort === 'alpha' ) {
                rpFilteredGroups.sort( function(a,b) { return a.postTitle.localeCompare( b.postTitle ); } );
            } else {
                rpFilteredGroups.sort( function(a,b) { return b.results.length - a.results.length; } );
            }

            rpCurrentPage = 1;
            _rpFilteredGroups = rpFilteredGroups; // keep module-level ref in sync
            rpRenderCurrentPage();
        }

        // ── Pagination ───────────────────────────────────────────────────
        _rpRenderPage = function() { rpRenderCurrentPage(); };
        function rpRenderCurrentPage() {
            if ( ! rpList ) return;

            var start = ( rpCurrentPage - 1 ) * RP_PAGE_SIZE;
            var page  = rpFilteredGroups.slice( start, start + RP_PAGE_SIZE );

            rpList.innerHTML = page.map( buildRpGroupCard ).join( '' );

            // Wire up group collapse toggles.
            rpList.querySelectorAll( '.spellmatic-group-toggle' ).forEach( function( btn ) {
                btn.addEventListener( 'click', function() {
                    var body = btn.closest( '.spellmatic-post-group' )
                                  .querySelector( '.spellmatic-post-group-body' );
                    var open = body.style.display !== 'none';
                    body.style.display = open ? 'none' : '';
                    btn.setAttribute( 'aria-expanded', open ? 'false' : 'true' );
                    btn.querySelector( '.spellmatic-toggle-icon' ).textContent = open ? '\u25B6' : '\u25BC';
                } );
            } );

            rpRenderPagination();
        }

        function rpRenderPagination() {
            if ( ! rpPagination ) return;
            var totalPages = Math.ceil( rpFilteredGroups.length / RP_PAGE_SIZE );

            if ( totalPages <= 1 ) {
                rpPagination.classList.add( 'hidden' );
                rpPagination.innerHTML = '';
                return;
            }

            rpPagination.classList.remove( 'hidden' );

            var html = '';
            html += '<button class="spellmatic-page-btn" data-page="' + ( rpCurrentPage - 1 ) + '" ' +
                    ( rpCurrentPage === 1 ? 'disabled' : '' ) + '>&laquo; Prev</button> ';

            for ( var p = 1; p <= totalPages; p++ ) {
                html += '<button class="spellmatic-page-btn' + ( p === rpCurrentPage ? ' is-current' : '' ) + '" data-page="' + p + '">' + p + '</button> ';
            }

            html += '<button class="spellmatic-page-btn" data-page="' + ( rpCurrentPage + 1 ) + '" ' +
                    ( rpCurrentPage === totalPages ? 'disabled' : '' ) + '>Next &raquo;</button>';

            rpPagination.innerHTML = html;

            rpPagination.querySelectorAll( '.spellmatic-page-btn:not([disabled])' ).forEach( function( btn ) {
                btn.addEventListener( 'click', function() {
                    rpCurrentPage = parseInt( btn.dataset.page, 10 );
                    rpRenderCurrentPage();
                    rpContainer.scrollIntoView( { behavior: 'smooth', block: 'start' } );
                } );
            } );
        }

        // ── Wire up action delegation on the results page container ──────
        rpList.addEventListener( 'click', handleResultAction );

        // ── Boot ─────────────────────────────────────────────────────────
        initRpFilterBar();
        rpRenderCurrentPage();
    }

    /** Append AI-found errors to the results list using grouped cards (same style as scan results). */
    function appendAiResults( errors ) {
        if ( ! resultsList || ! errors.length ) return;

        resultsContainer.classList.remove( 'hidden' );

        // Insert divider only once.
        if ( ! resultsList.querySelector( '.spellmatic-ai-divider' ) ) {
            const divider = document.createElement( 'div' );
            divider.className = 'spellmatic-ai-divider';
            divider.innerHTML = '<span>✦ AI-detected issues</span>';
            resultsList.appendChild( divider );
        }

        // Group AI errors by post_id.
        const aiGroupMap = {};
        errors.forEach( function( err ) {
            const pid = String( err.post_id );
            if ( ! aiGroupMap[ pid ] ) {
                const meta = postMeta[ pid ] || {};
                aiGroupMap[ pid ] = {
                    postId:    pid,
                    postTitle: err.post_title || meta.title || ( 'Post #' + pid ),
                    postType:  err.post_type  || meta.post_type || 'post',
                    pm:        {
                        permalink:         meta.permalink        || '',
                        builder_edit_url:  meta.builder_edit_url || '',
                        builder_edit_label:meta.builder_edit_label|| '',
                    },
                    results:   [],
                };
            }
            aiGroupMap[ pid ].results.push( err );
        } );

        const aiGroups = Object.values( aiGroupMap );

        // Build each group card using a variant of buildGroupCard with AI badge.
        aiGroups.forEach( function( group ) {
            const pid       = group.postId;
            const pm        = group.pm;
            const errCount  = group.results.length;
            const typeLabel = postTypes[ group.postType ] || ucfirst( group.postType );

            const editUrl  = escAttr( adminUrl + 'post.php?post=' + pid + '&action=edit' );
            const viewUrl  = pm.permalink ? escAttr( pm.permalink ) : '';

            const postLinks =
                ( viewUrl ? '<a href="' + viewUrl + '" target="_blank" class="spellmatic-post-link">View</a> <span class="spellmatic-link-sep">&middot;</span> ' : '' ) +
                '<a href="' + editUrl + '" target="_blank" class="spellmatic-post-link">Edit</a>';

            const errorRows = group.results.map( function( r ) {
                return buildAiErrorRow( r );
            } ).join( '' );

            const cardHtml =
                '<div class="spellmatic-post-group spellmatic-post-group--ai" data-post-id="' + escAttr( pid ) + '" ' +
                     'data-post-type="' + escAttr( group.postType ) + '" ' +
                     'data-error-count="' + errCount + '">' +

                    '<div class="spellmatic-post-group-header">' +
                        '<button class="spellmatic-group-toggle" aria-expanded="true" title="Collapse">' +
                            '<span class="spellmatic-toggle-icon">&#9660;</span>' +
                        '</button>' +
                        '<span class="spellmatic-type-badge spellmatic-type-badge--' + escAttr( group.postType ) + '">' +
                            escHtml( typeLabel ) +
                        '</span>' +
                        '<span class="spellmatic-badge spellmatic-badge--ai">[AI]</span>' +
                        '<span class="spellmatic-group-title">' + escHtml( group.postTitle ) + '</span>' +
                        '<span class="spellmatic-group-count">' + errCount + ' error' + ( errCount !== 1 ? 's' : '' ) + '</span>' +
                        '<span class="spellmatic-post-links">' + postLinks + '</span>' +
                    '</div>' +

                    '<div class="spellmatic-post-group-body">' +
                        errorRows +
                    '</div>' +

                '</div>';

            const cardEl = document.createElement( 'div' );
            cardEl.innerHTML = cardHtml;
            const groupEl = cardEl.firstElementChild;

            // Wire collapse toggle.
            const toggleBtn = groupEl.querySelector( '.spellmatic-group-toggle' );
            if ( toggleBtn ) {
                toggleBtn.addEventListener( 'click', function() {
                    const body = groupEl.querySelector( '.spellmatic-post-group-body' );
                    const open = body.style.display !== 'none';
                    body.style.display = open ? 'none' : '';
                    toggleBtn.setAttribute( 'aria-expanded', open ? 'false' : 'true' );
                    toggleBtn.querySelector( '.spellmatic-toggle-icon' ).textContent = open ? '\u25B6' : '\u25BC';
                } );
            }

            resultsList.appendChild( groupEl );
        } );
    }

    /** Build an individual AI error row (uses same style as scan-page buildErrorRow with AI badge). */
    function buildAiErrorRow( r ) {
        var errorType  = r.error_type || 'grammar';
        var original   = r.original   || '';
        var suggestion = r.suggestion || '';
        var context    = r.context    || '';

        var highlightedContext = context
            ? escHtml( context ).replace(
                new RegExp( '(' + escRegex( original ) + ')', 'gi' ),
                '<mark class="spellmatic-error-mark">$1</mark>'
              )
            : '';

        return (
            '<div class="spellmatic-result-row spellmatic-result-row--ai" data-id="' + escAttr( r.id || '' ) + '" data-suggestion="' + escAttr( suggestion ) + '" data-original="' + escAttr( original ) + '">' +

                '<div class="spellmatic-result-meta">' +
                    '<span class="spellmatic-badge spellmatic-badge--' + escAttr( errorType ) + '">' +
                        ucfirst( errorType ) +
                    '</span>' +
                    ( isHighConfidence( r )
                        ? '<span class="spellmatic-badge spellmatic-badge--confidence-high" title="Spelling error with a close suggestion — safe to auto-fix">High</span>'
                        : '<span class="spellmatic-badge spellmatic-badge--confidence-low" title="Grammar, style, or ambiguous — review manually">Low</span>'
                    ) +
                    '<span class="spellmatic-badge spellmatic-badge--ai">[AI]</span>' +
                '</div>' +

                ( highlightedContext
                    ? '<div class="spellmatic-result-context">' +
                        '<span class="spellmatic-context-label">Context:</span> ' +
                        '&ldquo;' + highlightedContext + '&rdquo;' +
                      '</div>'
                    : '' ) +

                '<div class="spellmatic-fix-row">' +
                    '<span class="spellmatic-original-word">' + escHtml( original ) + '</span>' +
                    '<span class="spellmatic-arrow-icon">&#8594;</span>' +
                    '<input type="text" class="spellmatic-fix-input" ' +
                        'value="' + escAttr( suggestion ) + '" ' +
                        'placeholder="' + ( suggestion ? escAttr( suggestion ) : 'type correction…' ) + '" ' +
                        'data-original="' + escAttr( original ) + '" ' +
                        'aria-label="Correction for ' + escAttr( original ) + '" />' +
                    ( r.id
                        ? '<button class="button button-primary spellmatic-fix-btn" ' +
                            'data-id="' + escAttr( r.id ) + '" data-action="fix">Apply Fix</button>' +
                          '<button class="button spellmatic-dismiss-btn" ' +
                            'data-id="' + escAttr( r.id ) + '" data-action="dismiss">Dismiss</button>'
                        : '' ) +
                    '<button class="button spellmatic-dict-add-inline" ' +
                        'data-word="' + escAttr( original ) + '" data-action="add-to-dictionary" ' +
                        'title="Add to dictionary and dismiss all occurrences">' +
                        '<span class="dashicons dashicons-book" style="font-size:14px;line-height:1.8;margin-right:2px;"></span>Dictionary</button>' +
                '</div>' +

            '</div>'
        );
    }

    /** Human-readable provider name. */
    function providerLabel( provider ) {
        const labels = {
            openai:     'OpenAI',
            claude:     'Claude',
            gemini:     'Gemini',
            grok:       'Grok',
            openrouter: 'OpenRouter',
        };
        return labels[ provider ] || provider || 'AI';
    }

    // ── Shared status helper ──────────────────────────────────────────────────
    function setStatus( el, msg, type ) {
        if ( ! el ) return;
        el.textContent  = msg;
        el.className    = 'spellmatic-dict-status' +
            ( type === 'success' ? ' spellmatic-dict-status--success' : '' ) +
            ( type === 'error'   ? ' spellmatic-dict-status--error'   : '' );
    }

    // ══════════════════════════════════════════════════════════════════════════
    // ── Harper Grammar Engine Revert Panel ────────────────────────────────────
    // ══════════════════════════════════════════════════════════════════════════
    ( function initHarperRevertPanel() {
        const revertBtn  = document.getElementById( 'spellmatic-harper-revert-btn' );
        const statusEl   = document.getElementById( 'spellmatic-harper-update-status' );
        const versionEl  = document.getElementById( 'spellmatic-harper-active-version' );

        if ( ! revertBtn ) return; // Not on the Open Source page or no upgrade present.

        revertBtn.addEventListener( 'click', async function() {
            if ( ! confirm( 'Revert to the bundled grammar engine (v' + ( window.spellmatic.harper?.bundled || '1.2.0' ) + ')?\n\nThe upgraded file will be removed from your server.' ) ) return;

            if ( statusEl ) setStatus( statusEl, 'Reverting…', '' );
            revertBtn.disabled = true;

            try {
                const res = await fetch( restUrl + '/harper/revert', {
                    method: 'POST',
                    headers: { 'X-WP-Nonce': nonce }
                } );
                const data = await res.json();

                if ( data.success ) {
                    if ( statusEl ) setStatus( statusEl, 'Reverted to bundled v' + data.version + '. Reload to apply.', 'success' );
                    if ( versionEl ) versionEl.textContent = data.version;
                    revertBtn.style.display = 'none';
                } else {
                    if ( statusEl ) setStatus( statusEl, data.error || 'Revert failed.', 'error' );
                }
            } catch ( err ) {
                if ( statusEl ) setStatus( statusEl, 'Revert failed: ' + err.message, 'error' );
            } finally {
                revertBtn.disabled = false;
            }
        } );
    } )();

} )();
