<?php
/**
 * Admin class - handles admin interface and settings
 * 
 * @package LangForge
 */

if (!defined('ABSPATH')) {
    exit;
}

// IDE hints for autoloaded classes
if (false) {
    class LF_Core {}
    class LF_String_Translation {}
    class LF_Menu_Translation {}
}

// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- LF_ is the established plugin class prefix
class LF_Admin {
    
    private static $instance = null;
    
    public static function instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    private function __construct() {
        $this->init_hooks();
    }
    
    private function init_hooks() {
        add_action('admin_menu', [$this, 'add_admin_menu']);
        add_action('admin_menu', [$this, 'reorder_submenu'], PHP_INT_MAX);
        add_action('admin_head', [$this, 'reorder_submenu'], 1);
        // hide sub-tab slugs from the sidebar via CSS.
        add_action('admin_head', [$this, 'hide_grouped_sidebar_items']);
        // follow-up: keep the hub's sidebar item highlighted on EVERY
        // sub-tab, not just the hub-parent slug. See highlight_hub_in_sidebar.
        add_filter('submenu_file', [$this, 'highlight_hub_in_sidebar'], 10, 2);
        add_action('admin_init', [$this, 'register_settings']);
        // LF audit 2026-04-28: glossary CSV export must run before
        // any admin chrome renders, otherwise headers are already
        // sent and the .csv response gets the WP admin <head> bolted
        // onto it. admin_init fires before admin-header.php.
        add_action('admin_init', [$this, 'maybe_handle_glossary_export']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
        add_filter('admin_body_class', [$this, 'add_body_class']);
        // branded header on every Lang Forge admin screen.
        add_action('in_admin_header', [$this, 'render_branded_header']);

        // Add settings link to plugins page
        add_filter('plugin_action_links_' . LF_PLUGIN_BASENAME, [$this, 'add_settings_link']);

        // Dashboard widget
        add_action('wp_dashboard_setup', [$this, 'add_dashboard_widget']);

        // backend-driven bulk AI translate for String Translation —
        // translates EVERY empty string in a domain (not just the visible
        // page) and saves directly to DB, so pagination doesn't drop
        // in-flight AI output.
        add_action('wp_ajax_lf_ai_translate_strings_bulk', [$this, 'ajax_ai_translate_strings_bulk']);

        // Tester feedback 2026-04-18: when a language is removed from
        // Settings and posts in that language exist, prompt the admin to
        // decide their fate (mirrors how WordPress handles user deletion).
        add_action('admin_notices',                       [$this, 'render_removed_language_prompt']);
        add_action('wp_ajax_lf_resolve_removed_language', [$this, 'ajax_resolve_removed_language']);

        // LF audit 2026-04-28 (R2-CP1): when WPML's core (SitePress) is
        // active alongside LangForge, both plugins fight over language
        // routing / `<html lang>` / post meta and produce unpredictable
        // frontend output. Warn the site owner everywhere LangForge
        // chrome shows up so they can deactivate WPML or migrate via
        // Lang Forge → Migration → WPML.
        add_action('admin_notices', [$this, 'render_wpml_coexistence_notice']);
    }

    /**
 * bulk AI-translate all strings in a domain across all active
 * target languages, save each translation directly to the strings table.
 * Returns a summary the UI can reload after.
 */
    public function ajax_ai_translate_strings_bulk() {
        check_ajax_referer('lf_admin_nonce', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Permission denied.', 'langforge')]);
        }
        if (!class_exists('LF_License') || !LF_License::can('ai_translation')) {
            wp_send_json_error(['message' => __('AI translation requires an active Lang Forge Pro license.', 'langforge')]);
        }

        if (!class_exists('LF_AI_Client')) {
            wp_send_json_error(['message' => __('AI translation requires the Lang Forge Pro add-on.', 'langforge')]);
        }
        $ai = LF_AI_Client::instance();
        if (!$ai->is_configured()) {
            wp_send_json_error(['message' => __('AI translation not configured.', 'langforge')]);
        }

        $domain = sanitize_text_field(wp_unslash($_POST['domain'] ?? ''));
        if (empty($domain)) {
            wp_send_json_error(['message' => __('Domain is required.', 'langforge')]);
        }

        // Mode: 'missing' (default) translates only empty slots; 'all' regenerates
        // every slot — overwriting existing values AND bypassing Translation Memory
        // ($force) so it truly re-runs the AI instead of re-applying a cached value.
        $mode  = (sanitize_key(wp_unslash($_POST['mode'] ?? 'missing')) === 'all') ? 'all' : 'missing';
        $force = ($mode === 'all');

        $default_lang    = LF_Core::instance()->get_default_language();
        $active_languages = LF_Core::instance()->get_active_languages();

        // Fetch every unique source string in the domain. Use the same dedup
        // logic as the UI (earliest-id row wins as the source).
        $all = $this->get_unique_strings($domain);
        if (empty($all)) {
            wp_send_json_error(['message' => __('No strings found in this domain.', 'langforge')]);
        }

        $translator = LF_String_Translation::instance();
        $total_done = 0;
        $total_skipped = 0;
        $errors = [];

        foreach ($active_languages as $target_lang => $lang_info) {
            if ($target_lang === $default_lang) continue;

            // Collect source-text. In 'missing' mode skip slots that already have a
            // translation; in 'all' mode include them so they get regenerated.
            $segments = [];
            foreach ($all as $name => $data) {
                if ($mode !== 'all' && !empty($data['translations'][$target_lang])) continue;
                $source_text = $data['translations'][$data['source_lang']] ?? '';
                if ($source_text === '') continue;
                $segments[] = ['key' => $name, 'text' => $source_text];
            }
            if (empty($segments)) continue;

            // 20-segment batches to stay inside token limits.
            $batches = array_chunk($segments, 20);
            foreach ($batches as $batch) {
                $result = $ai->translate_bulk($batch, $default_lang, $target_lang, $force);
                if (is_wp_error($result)) {
                    $errors[] = $target_lang . ': ' . $result->get_error_message();
                    continue;
                }
                if (!is_array($result)) continue;
                foreach ($result as $name => $translated) {
                    if (!is_string($translated) || $translated === '') { $total_skipped++; continue; }
                    $translator->save_translation($name, $domain, $target_lang, $translated);
                    $total_done++;
                }
            }
        }

        wp_send_json_success([
            'translated' => $total_done,
            'skipped'    => $total_skipped,
            'errors'     => $errors,
        ]);
    }

    public function add_body_class($classes) {
        $screen = get_current_screen();
        if ($screen && (strpos($screen->id, 'lf-') !== false || strpos($screen->id, 'langforge') !== false)) {
            $classes .= ' forge-admin lf-admin';
        }
        return $classes;
    }

    /**
 * Branded header (logo + brand gradient) on every Lang Forge admin
 * screen. Mirrors the SEO Forge implementation; CSS is shared via the
 * .lf-admin scope and pulls colors from --accent / --accent-active.
 */
    public function render_branded_header() {
        $screen = get_current_screen();
        if (!$screen || (strpos($screen->id, 'lf-') === false && strpos($screen->id, 'langforge') === false)) return;
        $version = defined('LF_VERSION') ? LF_VERSION : '';
        $logo    = LF_PLUGIN_URL . 'assets/logo.svg';
        ?>
        <div class="sf-brand-header">
            <div class="sf-brand-header__inner">
                <img class="sf-brand-header__logo" src="<?php echo esc_url($logo); ?>" alt="Lang Forge" width="40" height="40">
                <div class="sf-brand-header__text">
                    <div class="sf-brand-header__title">Lang Forge</div>
                    <div class="sf-brand-header__tagline"><?php esc_html_e('Multilingual WordPress without the friction.', 'langforge'); ?></div>
                </div>
                <?php if ($version): ?>
                <div class="sf-brand-header__version">v<?php echo esc_html($version); ?></div>
                <?php endif; ?>
                <?php if (class_exists('Forge_Credits_Widget')) { Forge_Credits_Widget::render_chip_in_header(); } ?>
            </div>
        </div>
        <?php
    }
    
    /**
 * Add admin menu pages
 */
    public function add_admin_menu() {
        // Forge Suite unified dashboard
        if (class_exists('Forge_Suite_Dashboard')) {
            Forge_Suite_Dashboard::maybe_register_menu();
        }

        add_menu_page(
            __('Lang Forge', 'langforge'),
            __('Lang Forge', 'langforge'),
            'manage_options',
            'lf-settings',
            [$this, 'render_settings_page'],
            'dashicons-translation',
            27
        );

        // PRO pages — show locked page on free plan.
        
        // Post-split (SPLIT_PLAN §3.4) the Pro classes (LF_Visual_Editor,
        // LF_Analytics, LF_Glossary, LF_TM_Admin, LF_Import_Export) live in
        // langforge-pro. On a Free-only site the renderer class is absent,
        // so the inner callback would fatal if it dereferenced
        // `LF_Visual_Editor::instance`. The pro_class_renderer helper
        // resolves the class lazily and falls back to the locked page when
        // it isn't loaded — same UX as is_free.
        $pro_class_renderer = function ($class_name, $method) {
            return function () use ($class_name, $method) {
                if (!class_exists($class_name)) {
                    // Pro add-on not installed at all — show upsell.
                    LF_Upgrade_Prompt::locked_page('');
                    return;
                }
                $instance = call_user_func([$class_name, 'instance']);
                call_user_func([$instance, $method]);
            };
        };

        $pro_gate = function ($feature_slug, $callback) use ($pro_class_renderer) {
            // Accept either a [Class, method] pair (preferred — Class is
            // resolved lazily) or a closure / direct callable (legacy).
            if (is_array($callback) && count($callback) === 2 && is_string($callback[0])) {
                $callback = $pro_class_renderer($callback[0], $callback[1]);
            }
            return function () use ($feature_slug, $callback) {
                // Gate per-capability, not per-plan: features now in
                // LF_License::FREE_FEATURES (glossary, woocommerce) open on a
                // Free-only site; true Pro features still fall through to the
                // upgrade screen. (2026-05-29 Free/Pro rebalance.)
                if (LF_License::can($feature_slug)) {
                    call_user_func($callback);
                } else {
                    LF_Upgrade_Prompt::locked_page($feature_slug);
                }
            };
        };

        // sidebar restructure (2026-05-05). Tester wants the
        // hub model:
        
        // 1 Languages (start page — no top tabs)
        // 2 Translations (hub: String Translation, Menu Translation, Visual Editor [PRO])
        // 3 Reports (hub: Translation Status, Analytics [PRO])
        // 4 Tools (hub: Glossary [PRO], Translation Memory [PRO], Import/Export [PRO])
        // 5 Migration (already its own page)
        // 6 Settings (Languages page IS Settings today; tester wants
        // Languages renamed to be clearly the start page)
        // + Freemius License + Contact Us at the end
        
        // Languages keeps slug `lf-settings` because the renderer is
        // bound there and Freemius / docs deep links know that slug.
        // It's just relabelled "Languages" + reordered to position 1
        // by reorder_submenu below.
        $pro_badge_html = ' <span style="background:#fff4e5;color:#b45309;border:1px solid #fcd9a8;border-radius:3px;font-size:10px;font-weight:700;padding:1px 5px;letter-spacing:.4px;text-transform:uppercase;line-height:1;">PRO</span>';
        // Per-capability badge: pass the feature slug so menu items whose
        // feature is now Free (glossary, woocommerce) render with no PRO badge.
        // Calling with no slug keeps the legacy plan-wide behavior.
        $pro_menu_title = function ($label, $feature = null) use ($pro_badge_html) {
            $locked = $feature !== null
                ? (class_exists('LF_License') && !LF_License::can($feature))
                : (class_exists('LF_License') ? LF_License::is_free() : false);
            return $label . ($locked ? $pro_badge_html : '');
        };

        // Languages — visible top sub-entry. Renderer is the existing
        // settings page (which manages site languages).
        add_submenu_page('lf-settings', __('Languages', 'langforge'),    __('Languages', 'langforge'),    'manage_options', 'lf-settings',         [$this, 'render_settings_page']);

        // Hub parents (visible). Each dispatches to the hub's first
        // sub-tab via the canonical-render method below.
        add_submenu_page('lf-settings', __('Translations', 'langforge'), __('Translations', 'langforge'), 'manage_options', 'lf-translations-hub', [$this, 'render_hub_translations']);
        add_submenu_page('lf-settings', __('Reports', 'langforge'),      __('Reports', 'langforge'),      'manage_options', 'lf-reports-hub',      [$this, 'render_hub_reports']);
        add_submenu_page('lf-settings', __('Tools', 'langforge'),        __('Tools', 'langforge'),        'manage_options', 'lf-tools-hub',        [$this, 'render_hub_tools']);

        // Sub-tab pages — registered for deep-link survival, hidden
        // from the sidebar via `hide_grouped_sidebar_items`.
        add_submenu_page('lf-settings', __('String Translation', 'langforge'),  __('String Translation', 'langforge'),  'manage_options', 'lf-strings',       [$this, 'render_strings_page']);
        add_submenu_page('lf-settings', __('Menu Translation', 'langforge'),    __('Menu Translation', 'langforge'),    'manage_options', 'lf-menus',         [LF_Menu_Translation::instance(), 'render_menu_page']);
        add_submenu_page('lf-settings', __('Visual Editor', 'langforge'),       $pro_menu_title(__('Visual Editor', 'langforge'), 'visual_editor'),       'edit_posts',     'lf-editor',        $pro_gate('visual_editor',     ['LF_Visual_Editor', 'render_page']));
        add_submenu_page('lf-settings', __('Translation Status', 'langforge'),  __('Translation Status', 'langforge'),  'manage_options', 'lf-status',        [$this, 'render_status_page']);
        add_submenu_page('lf-settings', __('Analytics', 'langforge'),           $pro_menu_title(__('Analytics', 'langforge'), 'analytics'),           'manage_options', 'lf-analytics',     $pro_gate('analytics',         ['LF_Analytics', 'render_page']));
        add_submenu_page('lf-settings', __('Glossary', 'langforge'),            $pro_menu_title(__('Glossary', 'langforge'), 'glossary'),            'manage_options', 'lf-glossary',      $pro_gate('glossary',          [$this, 'render_glossary_page']));
        add_submenu_page('lf-settings', __('Translation Memory', 'langforge'),  $pro_menu_title(__('Translation Memory', 'langforge'), 'translation_memory'),  'manage_options', 'lf-tm',            $pro_gate('translation_memory',['LF_TM_Admin', 'render_page']));
        add_submenu_page('lf-settings', __('Import / Export', 'langforge'),     $pro_menu_title(__('Import / Export', 'langforge'), 'import_export'),     'manage_options', 'lf-import-export', $pro_gate('import_export',     ['LF_Import_Export', 'render_page']));

        // v2 (2026-05-05): tester flagged that "Settings" was
        // missing from the LangForge sidebar. The current `lf-settings`
        // slug renders Languages (which is the start page); Settings
        // is a separate visible entry that also renders the settings
        // page so the user has a labelled "Settings" link, plus we
        // get a stable place to deep-link general plugin settings.
        add_submenu_page('lf-settings', __('Settings', 'langforge'), __('Settings', 'langforge'), 'manage_options', 'lf-plugin-settings', [$this, 'render_settings_page']);
    }

    /**
 * hub-parent renderers dispatch to the canonical first
 * sub-tab so the user lands on a real page when they click
 * "Translations" / "Reports" / "Tools" in the WP sidebar. The
 * top-tab strip shows only the hub's siblings (rendered by
 * each child page via `LF_Admin_Tabs::render`).
 */
    public function render_hub_translations() {
        // String Translation is the canonical landing for Translations.
        $this->render_strings_page();
    }
    public function render_hub_reports() {
        // Translation Status is the canonical landing for Reports.
        $this->render_status_page();
    }
    public function render_hub_tools() {
        $this->render_tools_utilities_page();
    }

    public function render_tools_utilities_page() {
        if (isset($_GET['action']) && $_GET['action'] === 'flush_rules') {
            flush_rewrite_rules();
            echo '<div class="notice notice-success"><p>' . esc_html__('Rewrite rules flushed.', 'langforge') . '</p></div>';
        }

        if (isset($_GET['action']) && $_GET['action'] === 'migrate_wpml' && isset($_GET['_wpnonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'lf_migrate_wpml')) {
            $result = $this->migrate_wpml_data();
            if ($result['success']) {
                /* translators: %d: number of translation groups imported from WPML. */
                echo '<div class="notice notice-success"><p>' . sprintf(esc_html__('Migration complete! Imported %d translation groups.', 'langforge'), esc_html($result['count'])) . '</p></div>';
            } else {
                echo '<div class="notice notice-error"><p>' . esc_html__("Migration failed: ", "langforge") . esc_html($result['error']) . '</p></div>';
            }
        }
        ?>
        <div class="wrap lf-settings-wrap">
            <?php LF_Admin_Tabs::render('lf-tools-hub'); ?>
            <div class="lf-settings-header">
                <h1><span class="dashicons dashicons-admin-tools" style="margin-right:8px;"></span><?php esc_html_e('Tools', 'langforge'); ?></h1>
            </div>

            <style>
            .lf-settings-wrap{max-width:860px;}
            .lf-settings-header{margin-bottom:24px;padding-bottom:16px;border-bottom:2px solid var(--avl-primary);}
            .lf-settings-header h1{display:flex;align-items:center;font-size:22px;font-weight:700;margin:0;}
            .lf-settings-header .dashicons{color:var(--avl-primary);font-size:24px;width:24px;height:24px;}
            .lf-card{background:var(--avl-bg-white);border:1px solid var(--avl-border);border-radius:12px;box-shadow:var(--avl-shadow);overflow:visible;}
            .lf-card-head{display:flex;align-items:center;gap:10px;padding:16px 20px;border-bottom:1px solid var(--avl-border);background:var(--avl-bg);border-radius:12px 12px 0 0;}
            .lf-card-head .dashicons{color:var(--avl-primary);font-size:18px;width:18px;height:18px;}
            .lf-card-head h2{margin:0;font-size:15px;font-weight:600;}
            .lf-card-body{padding:20px;}
            .lf-tool-btn{display:inline-flex;align-items:center;gap:6px;padding:9px 16px;background:var(--avl-bg-white);border:1px solid var(--avl-border);border-radius:8px;font-size:14px;font-weight:500;color:var(--avl-text);cursor:pointer;transition:all 0.15s;text-decoration:none;}
            .lf-tool-btn:hover{border-color:var(--avl-primary);color:var(--avl-primary);background:var(--avl-primary-light);}
            .lf-tool-btn .dashicons{font-size:16px;width:16px;height:16px;color:var(--avl-primary);}
            .lf-card select{width:100%!important;padding:9px 12px!important;border:1px solid var(--avl-border)!important;border-radius:8px!important;font-size:14px!important;background-color:var(--avl-bg-white)!important;box-sizing:border-box!important;height:auto!important;line-height:1.4!important;}
            </style>

            <div class="lf-card" style="margin-top:20px;">
                <div class="lf-card-head">
                    <span class="dashicons dashicons-admin-tools"></span>
                    <h2><?php esc_html_e('Tools', 'langforge'); ?><?php Forge_Docs_Link::render('langforge', 'recipe-migrating-from-wpml-to-lang-forge', __('Tools', 'langforge'), __('Flush rewrite rules, populate TM, import WPML data.', 'langforge')); ?></h2>
                </div>
                <div class="lf-card-body">
                    <div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;">
                        <a href="<?php echo esc_url(admin_url('admin.php?page=lf-tools-hub&action=flush_rules')); ?>" class="lf-tool-btn">
                            <span class="dashicons dashicons-update"></span>
                            <?php esc_html_e('Flush Rewrite Rules', 'langforge'); ?>
                        </a>
                        <?php if (LF_License::instance()->can('translation_memory')): ?>
                        <button type="button" class="lf-tool-btn" id="avl-populate-tm">
                            <span class="dashicons dashicons-database"></span>
                            <?php esc_html_e('Populate Translation Memory', 'langforge'); ?>
                        </button>
                        <?php else: ?>
                        <span class="lf-tool-btn" style="opacity:0.5;cursor:default;pointer-events:none;">
                            <span class="dashicons dashicons-database"></span>
                            <?php esc_html_e('Populate Translation Memory', 'langforge'); ?>
                            <span class="lf-pro-badge" style="font-size:14px;background:#0EC49E;color:#fff;padding:1px 5px;border-radius:3px;margin-left:4px;">PRO</span>
                        </span>
                        <?php endif; ?>
                        <?php if ($this->wpml_data_exists()): ?>
                        <a href="<?php echo esc_url(wp_nonce_url(admin_url('admin.php?page=lf-tools-hub&action=migrate_wpml'), 'lf_migrate_wpml')); ?>" class="lf-tool-btn" onclick="return confirm('<?php esc_attr_e('Import all WPML translation relationships?', 'langforge'); ?>');">
                            <span class="dashicons dashicons-migrate"></span>
                            <?php esc_html_e('Import WPML Data', 'langforge'); ?>
                        </a>
                        <?php endif; ?>
                    </div>
                    <span id="avl-tm-result" style="display:block;margin-top:8px;font-size:14px;"></span>

                    <div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--avl-border);">
                        <?php if (LF_License::instance()->can('bulk_actions')): ?>
                            <?php LF_Bulk_Actions::render_duplicate_button(); ?>
                        <?php else: ?>
                            <h2 style="display:flex;align-items:center;gap:8px;">
                                <?php esc_html_e('One-Click Site Duplication', 'langforge'); ?>
                                <span class="lf-pro-badge" style="font-size:14px;background:#0EC49E;color:#fff;padding:2px 6px;border-radius:4px;">PRO</span>
                            </h2>
                            <p class="description"><?php esc_html_e('Create draft copies of all content for a new language.', 'langforge'); ?></p>
                            <div style="margin-top:10px;opacity:0.5;pointer-events:none;">
                                <select disabled><option><?php esc_html_e('Select language', 'langforge'); ?></option></select>
                                <button class="button" disabled><?php esc_html_e('Duplicate Only', 'langforge'); ?></button>
                            </div>
                            <p style="margin-top:8px;">
                                <a href="<?php echo esc_url(LF_License::upgrade_url()); ?>" style="color:#0EC49E;font-weight:600;"><?php esc_html_e('Upgrade to Pro', 'langforge'); ?> →</a>
                            </p>
                        <?php endif; ?>
                    </div>
                </div>
            </div>

            <script>
            jQuery('#avl-populate-tm').on('click', function() {
                var $btn = jQuery(this);
                $btn.prop('disabled', true).css('opacity','0.6');
                jQuery('#avl-tm-result').html('<span style="color:var(--avl-text-secondary);"><?php echo esc_js(__('Processing...', 'langforge')); ?></span>');
                jQuery.ajax({
                    url: '<?php echo esc_url(rest_url('langforge/v1/tm/populate')); ?>',
                    method: 'POST',
                    headers: { 'X-WP-Nonce': '<?php echo esc_html(wp_create_nonce('wp_rest')); ?>' },
                    success: function(r) {
                        $btn.prop('disabled', false).css('opacity','1');
                        jQuery('#avl-tm-result').html('<span style="color:var(--avl-success);">&#10003; ' + r.message + '</span>');
                    }
                });
            });
            </script>
        </div>
        <?php
    }

    /**
 * hide every sub-tab slug from the WP sidebar — they're
 * still registered (so deep links survive) but reachable only
 * through the in-page hub tab strip.
 */
    public function hide_grouped_sidebar_items() {
        // Hide internal sub-tabs that are duplicated as tabs inside the hub
        // pages (Translations / Reports / Tools). They stay registered, so deep
        // links and the in-hub tab strip keep working — they're just not
        // separate sidebar entries.
        
        // (2026-05-31): the Pro sub-tabs (Visual Editor, Analytics,
        // Glossary, Translation Memory, Import/Export) were previously left
        // visible in the sidebar (2026-05-27), but each already lives inside its
        // hub (Visual Editor → Translations; Analytics → Reports; Glossary /
        // Translation Memory / Import-Export → Tools), so the standalone entries
        // duplicated the navigation. Hide them too; the pages stay reachable
        // from the hub tab strip (a locked Pro tab still shows the upgrade
        // banner via $pro_gate).
        $hidden = [
            'lf-strings', 'lf-menus',     // Translations hub Free sub-tabs
            'lf-status',                  // Reports hub Free sub-tab
            'lf-migration',               // Tools hub Free sub-tab (v2)
            'lf-editor',                  // Visual Editor [PRO] — Translations hub
            'lf-analytics',               // Analytics [PRO] — Reports hub
            'lf-glossary',                // Glossary [PRO] — Tools hub
            'lf-tm',                      // Translation Memory [PRO] — Tools hub
            'lf-import-export',           // Import / Export [PRO] — Tools hub
        ];
        $selectors = [];
        foreach ($hidden as $slug) {
            $selectors[] = '#toplevel_page_lf-settings .wp-submenu li:has(> a[href$="page=' . $slug . '"])';
        }
        echo '<style id="langforge-hide-grouped-submenus">' .
             implode(',', $selectors) . // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSS selectors built from hardcoded slug constants only; no dynamic/user data.
             '{display:none !important;}</style>';
    }

    /**
 * Keep the hub's sidebar item ("Translations" / "Reports" / "Tools")
 * highlighted while on ANY of its sub-tabs — not only the hub-parent slug.
 *
 * The sub-tab pages (lf-strings, lf-menus, lf-editor, lf-status,
 * lf-analytics, lf-glossary, lf-tm, lf-import-export, lf-migration) are
 * each registered as their OWN (hidden) submenu slug. Without this filter
 * WordPress marks the hidden sub-tab item as "current" and leaves the
 * visible hub parent un-highlighted on every tab except the first one
 * (where the URL is the hub-parent slug itself). Point `$submenu_file` at
 * the hub parent so the visible sidebar item stays active across the hub.
 *
 * @param string $submenu_file Submenu slug WP intends to mark current.
 * @param string $parent_file Parent (top-level) file — unused; all hub
 * pages share the `lf-settings` top level.
 * @return string
 */
    public function highlight_hub_in_sidebar($submenu_file, $parent_file = '') {
        if (!class_exists('LF_Admin_Tabs')) {
            return $submenu_file;
        }
        $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin UI state (current page), no data mutation.
        if ($page === '') {
            return $submenu_file;
        }
        $hub_key = LF_Admin_Tabs::hub_for_slug($page);
        if (!$hub_key) {
            return $submenu_file;
        }
        $hubs        = LF_Admin_Tabs::hubs();
        $parent_slug = isset($hubs[$hub_key]['parent']) ? $hubs[$hub_key]['parent'] : '';
        return $parent_slug !== '' ? $parent_slug : $submenu_file;
    }

    /**
 * Sidebar order for Lang Forge ends with: …, Migration, Contact Us,
 * License, Languages (Settings) — matches the canonical order across
 * all Forge plugins. Languages is the parent slug and effectively
 * the Settings page, so it's pinned last; other LF items keep their
 * relative order; Freemius-added Contact Us / License / Account /
 * Pricing are matched by title since their slugs vary.
 */
    public function reorder_submenu() {
        global $submenu;
        if (empty($submenu['lf-settings'])) return;

        // v2 (2026-05-05) canonical sidebar order — matches the
        // tester's expected sidebar exactly:
        // 10 Languages · 20 Translations · 30 Reports · 40 Tools ·
        // 50 Settings · 60 Contact Us · 70 License
        
        // Migration was previously at pos=50; tester flagged that as
        // wrong (Migration belongs inside the Tools hub, not as a
        // sidebar sibling). Migration now sinks to pos=900 (hidden
        // by `hide_grouped_sidebar_items`) and renders as the 4th
        // tab inside Tools via `LF_Admin_Tabs::hubs`.
        
        // The new `lf-plugin-settings` slug carries the "Settings"
        // label at pos=50 — tester explicitly flagged its absence
        
        // settings page Languages does, but is visibly labelled
        // "Settings" in the sidebar.
        $sorted = [];
        foreach ($submenu['lf-settings'] as $idx => $item) {
            $slug  = isset($item[2]) ? (string) $item[2] : '';
            $title = isset($item[0]) ? strtolower(wp_strip_all_tags((string) $item[0])) : '';
            $pos   = 500 + $idx; // unknowns sink to the bottom in registration order.

            if ($slug === 'lf-settings')                                                           $pos = 10;
            elseif ($slug === 'lf-translations-hub')                                               $pos = 20;
            elseif ($slug === 'lf-reports-hub')                                                    $pos = 30;
            elseif ($slug === 'lf-tools-hub')                                                      $pos = 40;
            elseif ($slug === 'lf-plugin-settings')                                                $pos = 50;
            elseif (strpos($title, 'contact') !== false || strpos($title, 'support') !== false)   $pos = 60;
            elseif (strpos($title, 'license') !== false  || strpos($title, 'account') !== false
                 || strpos($title, 'affiliation') !== false || strpos($title, 'pricing') !== false
                 || strpos($title, 'upgrade') !== false  || strpos($title, 'add-ons') !== false)  $pos = 70;
            // Hidden sub-tab slugs (incl. lf-migration which now lives inside Tools).
            elseif (in_array($slug, ['lf-strings','lf-menus','lf-editor','lf-status',
                                      'lf-analytics','lf-glossary','lf-tm','lf-import-export',
                                      'lf-migration'], true)) $pos = 900;

            $sorted[] = ['pos' => $pos, 'idx' => $idx, 'item' => $item];
        }
        usort($sorted, function ($a, $b) {
            if ($a['pos'] !== $b['pos']) return $a['pos'] - $b['pos'];
            return $a['idx'] - $b['idx'];
        });
        $submenu['lf-settings'] = array_map(function ($s) { return $s['item']; }, $sorted);
    }
    
    /**
 * Stream glossary CSV before any admin chrome echoes. Triggered
 * from admin_init so it fires before admin-header.php prints
 * <html>. Without this, the .csv blob gets the WP admin shell
 * appended to it and the browser saves an unparseable file.
 *
 * Added 2026-04-28 — backend export_csv lived in LF_Glossary
 * since the original PRO build, only the entry point was missing.
 */
    public function maybe_handle_glossary_export() {
        if (empty($_POST['lf_glossary_export'])) return;
        if (!isset($_POST['lf_glossary_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['lf_glossary_nonce'])), 'lf_add_glossary')) return;
        if (!current_user_can('manage_options')) return;
        if (!class_exists('LF_License') || !LF_License::instance()->can('glossary')) return;

        $from = isset($_POST['from']) ? sanitize_text_field(wp_unslash($_POST['from'])) : '';
        $to   = isset($_POST['to']) ? sanitize_text_field(wp_unslash($_POST['to'])) : '';
        if (!$from || !$to) {
            $default_lang = LF_Core::instance()->get_default_language();
            $languages    = LF_Core::instance()->get_active_languages();
            $from = $from ?: $default_lang;
            if (!$to) {
                foreach ($languages as $code => $_l) {
                    if ($code !== $default_lang) { $to = $code; break; }
                }
            }
        }
        $csv      = LF_Glossary::instance()->export_csv($from, $to);
        $filename = sprintf('langforge-glossary-%s-to-%s-%s.csv', $from, $to, gmdate('Y-m-d'));
        nocache_headers();
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');
        echo $csv; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built CSV download payload
        exit;
    }

    /**
 * Register plugin settings
 */
    public function register_settings() {
        register_setting('lf_settings', 'lf_default_language', [
            'type'              => 'string',
            'sanitize_callback' => 'sanitize_text_field',
        ]);
        register_setting('lf_settings', 'lf_active_languages', [
            'type'              => 'array',
            'sanitize_callback' => function ($val) {
                // Nested language-config array; sanitize keys and scalar leaves
                // without flattening the structure.
                if (!is_array($val)) {
                    return [];
                }
                $clean = [];
                foreach ($val as $code => $config) {
                    $code = sanitize_key($code);
                    if (is_array($config)) {
                        $clean[$code] = map_deep($config, 'sanitize_text_field');
                    } else {
                        $clean[$code] = sanitize_text_field($config);
                    }
                }
                return $clean;
            },
        ]);
        register_setting('lf_settings', 'lf_hide_default_language', [
            'type'              => 'boolean',
            'sanitize_callback' => function ($val) {
                return !empty($val) ? 1 : 0;
            },
        ]);
        register_setting('lf_settings', 'lf_url_format', [
            'type'              => 'string',
            'default'           => 'directory',
            'sanitize_callback' => function ($val) {
                return in_array($val, ['directory', 'parameter', 'subdomain', 'domain'], true)
                    ? $val
                    : 'directory';
            },
        ]);
        register_setting('lf_settings', 'lf_translation_model', [
            'type'              => 'string',
            'default'           => 'openai-mini',
            'sanitize_callback' => function ($val) {
                // Translation runs only on OpenAI GPT tiers. Anything else
                // is force-corrected to the default.
                return in_array($val, ['openai-mini', 'openai-gpt4'], true)
                    ? $val
                    : 'openai-mini';
            },
        ]);
    }
    
    /**
 * Enqueue admin scripts
 */
    public function enqueue_admin_scripts($hook) {
        // Admin styles — load on all pages so generic LangForge chrome looks right.
        // LF_VERSION is frozen at '1.0.0' so CSS edits don't bust the browser
        // cache on existing installs. Append filemtime so every save of
        // admin.css produces a new ?ver=..., same trick class-visual-editor
        // and class-language-switcher already use. (, 2026-04-21.)
        $css_path = LF_PLUGIN_DIR . 'assets/css/admin.css';
        $css_ver  = file_exists($css_path)
            ? LF_VERSION . '.' . filemtime($css_path)
            : LF_VERSION;
        wp_enqueue_style(
            'lf-admin',
            LF_PLUGIN_URL . 'assets/css/admin.css',
            [],
            $css_ver
        );

        // Restrict the admin JS (which binds global click handlers) to pages
        // where LangForge UI elements are actually present: LangForge menu pages,
        // post/page list screens, and post/page editor screens.
        // Loading it on all admin pages causes the document-level click handlers
        // to interfere with other plugins (e.g. FormForge builder).
        $screen        = get_current_screen();
        $screen_id     = $screen ? $screen->id : '';
        $screen_base   = $screen ? $screen->base : '';
        $page_param    = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin UI state (current page), no data mutation.

        $is_lf_page    = strpos($page_param, 'lf-') === 0;
        $is_post_list  = in_array($screen_base, ['edit', 'post', 'page'], true);
        $is_taxonomy   = $screen_base === 'edit-tags';

        $is_post_edit  = $screen_base === 'post'; // single post/page edit screen
        if (!$is_lf_page && !$is_post_list && !$is_taxonomy && !$is_post_edit) {
            return;
        }

        // Admin scripts
        $lf_js_ver = LF_VERSION . '.' . filemtime(LF_PLUGIN_DIR . 'assets/js/admin.js');
        wp_enqueue_script(
            'lf-admin',
            LF_PLUGIN_URL . 'assets/js/admin.js',
            ['jquery'],
            $lf_js_ver,
            true
        );

        wp_localize_script('lf-admin', 'lfAdmin', [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('lf_admin_nonce'),
            'forgeQueueNonce' => wp_create_nonce('forge_queue_nonce'),
            'strings' => [
                'confirm_delete' => __('Are you sure you want to delete this?', 'langforge'),
                'saving' => __('Saving...', 'langforge'),
                'saved' => __('Saved!', 'langforge'),
                'error' => __('Error occurred', 'langforge'),
                'qaUnsaved' => __('You have unsaved AI QA Check results. They will be lost and re-running will spend AI credits again.', 'langforge'),
                'fix' => __('Fix', 'langforge'),
                'changeSavedReload' => __('Change saved to translation. <a href="#" class="lf-qa-reload-link" style="color:#0EC49E;font-weight:600;">Refresh editor</a>', 'langforge'),
            ],
        ]);
    }
    
    /**
 * Add settings link to plugins page
 */
    public function add_settings_link($links) {
        $settings_link = '<a href="' . admin_url('admin.php?page=lf-settings') . '">' . __('Settings', 'langforge') . '</a>';
        array_unshift($links, $settings_link);
        return $links;
    }
    
    /**
 * Render main settings page
 */
    public function render_settings_page() {
        // Handle form submission
        if (isset($_POST['lf_save_settings']) && wp_verify_nonce(isset($_POST['lf_settings_nonce']) ? sanitize_text_field(wp_unslash($_POST['lf_settings_nonce'])) : '', 'lf_save_settings')) {
            $this->save_settings();
            echo '<div class="notice notice-success"><p>' . esc_html__('Settings saved.', 'langforge') . '</p></div>';
        }
        
        $default_language = get_option('lf_default_language', 'ru');
        $stored_active_languages = get_option('lf_active_languages', []);
        $active_languages = $stored_active_languages;
        $locked_languages = [];
        $hide_default = get_option('lf_hide_default_language', false);
        $all_languages = LF_Core::instance()->get_available_languages();
        $current_page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : 'lf-settings';
        $is_languages_page = ($current_page === 'lf-settings');
        if ($is_languages_page && class_exists('LF_License') && !LF_License::can('unlimited_languages')) {
            $active_languages = LF_Core::instance()->get_active_languages();
            $locked_languages = LF_Core::instance()->get_locked_languages();
        }
        ?>
        <div class="wrap lf-settings-wrap">
            <?php LF_Admin_Tabs::render($current_page); ?>
            <div class="lf-settings-header">
                <h1><span class="dashicons dashicons-translation" style="margin-right:8px;"></span><?php echo esc_html($is_languages_page ? __('Languages', 'langforge') : __('Lang Forge Settings', 'langforge')); ?></h1>
            </div>

            <?php $this->render_wpml_settings_conflict_callout(); ?>

            <?php
            // Free-plan over-limit banner.
            
            // NOTE: we deliberately do NOT use `class="notice notice-warning"` here.
            // WordPress core JavaScript (wp-admin/js/common.js -> handle_notice
            // relocation) moves any `.notice` element to right after the page's
            // `<h1>`. In LangForge's custom hub layout the `<h1>` lives inside
            // `.lf-settings-header` (a flex container), and WP's relocation
            // logic ends up placing the notice in a flex slot that collapses
            // to zero height — so the banner is in the DOM but visually
            // absent. Switching to a private class (`lf-overlimit-banner`)
            // bypasses the WP relocator and keeps our markup intact.
            if ($is_languages_page && class_exists('LF_License') && !LF_License::can('unlimited_languages')) {
                $lang_limit  = (int) LF_License::get_limit('languages');
                $lang_active = count((array) get_option('lf_active_languages', []));
                if ($lang_limit > 0 && $lang_active > $lang_limit) {
                    $excess      = $lang_active - $lang_limit;
                    $upgrade_url = esc_url(LF_License::instance()->upgrade_url());
                    $msg_count   = sprintf(
                        /* translators: 1: number of languages saved, 2: free-plan limit */
                        esc_html__('You have %1$d active languages, but the Free plan supports %2$d.', 'langforge'),
                        $lang_active,
                        $lang_limit
                    );
                    $msg_body    = sprintf(
                        /* translators: 1: excess count, 2: opening upgrade link, 3: closing link */
                        esc_html__('The extra %1$d language(s) are hidden from the language switcher, hreflang tags, and frontend rendering — your data is preserved and will reactivate the moment you %2$supgrade to Pro%3$s.', 'langforge'),
                        $excess,
                        '<a href="' . $upgrade_url . '" target="_blank" rel="noopener" style="font-weight:600;color:#0EC49E;">',
                        '</a>'
                    );
                    echo '<div class="lf-overlimit-banner" style="display:block;border:1px solid #fcd9a8;border-left:4px solid #FF6B35;background:#fff7ed;padding:12px 16px;margin:0 0 20px;border-radius:4px;">';
                    echo '<p style="margin:0 0 6px;font-weight:600;font-size:14px;color:#1e293b;">⚠ ' . $msg_count . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $msg_count built from esc_html__ format
                    echo '<p style="margin:0;color:#475569;font-size:13px;">' . $msg_body . '</p>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $msg_body built from esc_html__ format with intentional esc_url'd link markup
                    echo '</div>';
                }
            }
            ?>

            <form method="post" action="" id="lf-settings-form">
                <?php wp_nonce_field('lf_save_settings', 'lf_settings_nonce'); ?>
                <input type="hidden" name="lf_settings_context" value="<?php echo esc_attr($is_languages_page ? 'languages' : 'settings'); ?>">

                <div class="lf-settings-grid">

                <?php if ($is_languages_page): ?>
                <!-- ==================== LANGUAGES CARD ==================== -->
                <div class="lf-card">
                    <div class="lf-card-head">
                        <span class="dashicons dashicons-admin-site-alt3"></span>
                        <h2><?php esc_html_e('Active Languages', 'langforge'); ?><?php Forge_Docs_Link::render('langforge', 'adding-languages', __('Adding languages', 'langforge'), __('Pick a default, add translations, choose a URL format.', 'langforge')); ?></h2>
                    </div>
                    <div class="lf-card-body lf-card-body--flush">
                        <table class="lf-lang-table" id="lf-active-languages-table">
                            <thead>
                                <tr>
                                    <th><?php esc_html_e('Language', 'langforge'); ?></th>
                                    <th class="lf-col-sm"><?php esc_html_e('Code', 'langforge'); ?></th>
                                    <th class="lf-col-sm"><?php esc_html_e('Display', 'langforge'); ?></th>
                                    <th class="lf-col-center"><?php esc_html_e('Switcher', 'langforge'); ?></th>
                                    <th class="lf-col-center"><?php esc_html_e('Default', 'langforge'); ?></th>
                                    <th class="lf-col-xs"></th>
                                </tr>
                            </thead>
                            <tbody id="lf-active-languages-body">
                                <?php foreach ($active_languages as $code => $lang_data):
                                    $lang_info = isset($all_languages[$code]) ? $all_languages[$code] : $lang_data;
                                    $display_name = isset($lang_data['display_name']) ? $lang_data['display_name'] : $code;
                                    $show_in_switcher = isset($lang_data['show_in_switcher']) ? $lang_data['show_in_switcher'] : true;
                                    $is_default = ($default_language === $code);
                                ?>
                                <tr data-code="<?php echo esc_attr($code); ?>">
                                    <td>
                                        <input type="hidden" name="lf_languages[]" value="<?php echo esc_attr($code); ?>">
                                        <span class="lf-flag"><?php echo esc_html(self::get_flag_emoji($lang_info['flag'] ?? $code)); ?></span>
                                        <strong><?php echo esc_html($lang_info['native_name'] ?? $code); ?></strong>
                                        <span class="lf-english"><?php echo esc_html($lang_info['name'] ?? ''); ?></span>
                                        <?php if (!empty($lang_info['rtl'])): ?>
                                            <span class="lf-rtl-badge" title="<?php esc_attr_e('Right-to-left language — front-end, admin bar, and Visual Editor target column automatically flip to RTL.', 'langforge'); ?>">RTL</span>
                                        <?php endif; ?>
                                    </td>
                                    <td><code><?php echo esc_html($code); ?></code></td>
                                    <td><input type="text" name="lf_display_name[<?php echo esc_attr($code); ?>]" value="<?php echo esc_attr($display_name); ?>" class="lf-inp-sm" placeholder="<?php echo esc_attr($code); ?>"></td>
                                    <td class="lf-col-center"><input type="checkbox" name="lf_show_in_switcher[<?php echo esc_attr($code); ?>]" value="1" <?php checked($show_in_switcher); ?>></td>
                                    <td class="lf-col-center"><input type="radio" name="lf_default_language" value="<?php echo esc_attr($code); ?>" <?php checked($is_default); ?>></td>
                                    <td class="lf-col-center">
                                        <?php if (!$is_default): ?>
                                        <button type="button" class="lf-btn-remove" data-code="<?php echo esc_attr($code); ?>"><span class="dashicons dashicons-no-alt"></span></button>
                                        <?php endif; ?>
                                    </td>
                                </tr>
                                <?php endforeach; ?>
                                <?php foreach ($locked_languages as $code => $lang_data):
                                    $lang_info = isset($all_languages[$code]) ? $all_languages[$code] : $lang_data;
                                    $display_name = isset($lang_data['display_name']) ? $lang_data['display_name'] : $code;
                                ?>
                                <tr data-code="<?php echo esc_attr($code); ?>" class="lf-lang-locked">
                                    <td>
                                        <span class="lf-flag"><?php echo esc_html(self::get_flag_emoji($lang_info['flag'] ?? $code)); ?></span>
                                        <strong><?php echo esc_html($lang_info['native_name'] ?? $code); ?></strong>
                                        <span class="lf-english"><?php echo esc_html($lang_info['name'] ?? ''); ?></span>
                                        <span class="lf-pro-badge" style="font-size:12px;background:#0EC49E;color:#fff;padding:1px 5px;border-radius:3px;margin-left:6px;">PRO</span>
                                    </td>
                                    <td><code><?php echo esc_html($code); ?></code></td>
                                    <td><span class="lf-locked-value"><?php echo esc_html($display_name); ?></span></td>
                                    <td class="lf-col-center"><span class="lf-locked-value"><?php esc_html_e('Locked', 'langforge'); ?></span></td>
                                    <td class="lf-col-center"><span class="lf-locked-value">&mdash;</span></td>
                                    <td class="lf-col-center">
                                        <a href="<?php echo esc_url(LF_License::upgrade_url()); ?>" class="lf-upgrade-inline"><?php esc_html_e('Upgrade', 'langforge'); ?></a>
                                    </td>
                                </tr>
                                <?php endforeach; ?>
                                <?php if (empty($active_languages) && empty($locked_languages)): ?>
                                <tr id="lf-no-languages-row"><td colspan="6" class="lf-empty"><?php esc_html_e('No languages added yet.', 'langforge'); ?></td></tr>
                                <?php endif; ?>
                            </tbody>
                        </table>
                        <div class="lf-add-wrap">
                            <div class="lf-add-inner">
                                <span class="dashicons dashicons-plus-alt2 lf-add-icon"></span>
                                <input type="text" id="lf-add-language-input" placeholder="<?php esc_attr_e('Add a language...', 'langforge'); ?>" autocomplete="off">
                                <div id="lf-language-dropdown" class="lf-dropdown"></div>
                            </div>
                        </div>
                    </div>
                </div>

                <?php else: ?>
                <!-- ==================== URL SETTINGS CARD ==================== -->
                <div class="lf-card">
                    <div class="lf-card-head">
                        <span class="dashicons dashicons-admin-links"></span>
                        <h2><?php esc_html_e('URL Settings', 'langforge'); ?><?php Forge_Docs_Link::render('langforge', 'choosing-a-url-format', __('URL format', 'langforge'), __('Directory, parameter, subdomain or domain — pick how language appears in URLs.', 'langforge')); ?></h2>
                    </div>
                    <div class="lf-card-body">
                        <?php $url_format = get_option('lf_url_format', 'directory'); ?>
                        <div class="lf-url-options">
                            <?php
                            $url_opts = [
                                'directory' => ['icon' => 'category',      'label' => __('Directory', 'langforge'),  'example' => 'example.com/<b>en</b>/page'],
                                'parameter' => ['icon' => 'admin-generic', 'label' => __('Parameter', 'langforge'),  'example' => 'example.com/page<b>?lang=en</b>'],
                                'subdomain' => ['icon' => 'networking',    'label' => __('Subdomain', 'langforge'),  'example' => '<b>en</b>.example.com/page'],
                                'domain'    => ['icon' => 'admin-site',    'label' => __('Domain', 'langforge'),     'example' => 'example.<b>com</b> / example.<b>de</b>'],
                            ];
                            foreach ($url_opts as $val => $opt): ?>
                            <label class="lf-url-opt <?php echo $url_format === $val ? 'active' : ''; ?>">
                                <input type="radio" name="lf_url_format" value="<?php echo esc_attr($val); ?>" <?php checked($url_format, $val); ?>>
                                <span class="dashicons dashicons-<?php echo esc_attr($opt['icon']); ?>"></span>
                                <span class="lf-url-opt-text">
                                    <strong><?php echo esc_html($opt['label']); ?></strong>
                                    <span><?php echo $opt['example']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted hardcoded string literal with intentional <b> markup ?></span>
                                </span>
                            </label>
                            <?php endforeach; ?>
                        </div>

                        <?php
                        // Domain URL mode needs a per-language host map
                        // — without it, detect_language can't resolve the
                        // language from HTTP_HOST and the mode is dead. Only
                        // show the inputs when "Domain" is the active mode.
                        $domain_map = get_option('lf_domain_map', []);
                        if (!is_array($domain_map)) $domain_map = [];
                        ?>
                        <div class="lf-option-row lf-domain-map" data-show-when-format="domain" style="<?php echo $url_format === 'domain' ? '' : 'display:none;'; ?>">
                            <h3 class="lf-sub-heading" style="margin:8px 0 4px;"><?php esc_html_e('Per-language Domains', 'langforge'); ?></h3>
                            <p class="lf-hint" style="margin:0 0 10px;"><?php esc_html_e('Enter the domain (without scheme) that should serve each language. Required for the Domain mode to work — without it Lang Forge can\'t pick a language from the request host.', 'langforge'); ?></p>
                            <?php
                            // Domain mode keys the language off the request HOST, so it
                            // can only work when each language is served from a real,
                            // resolvable domain. On localhost it silently never switches
                            // (the switcher would point at e.g. example.ru, which doesn't
                            // resolve) — warn instead of leaving the user puzzled.
                            $lf_host = (string) wp_parse_url(home_url(), PHP_URL_HOST);
                            $lf_is_local = in_array($lf_host, ['localhost', '127.0.0.1', '::1'], true) || strpos($lf_host, '.') === false;
                            if ($lf_is_local):
                            ?>
                            <div style="border:1px solid #fcd9a8;border-left:4px solid #FF6B35;background:#fff7ed;padding:10px 14px;margin:0 0 12px;border-radius:6px;font-size:13px;color:#7c2d12;line-height:1.5;">
                                <strong>&#9888; <?php esc_html_e('Domain mode needs real, resolvable domains.', 'langforge'); ?></strong>
                                <?php printf(
                                    /* translators: %s = current site host */
                                    esc_html__('This site runs on %s, so the switcher can\'t change language by host here. To test locally, map each domain in /etc/hosts and add a matching virtual host; otherwise use Directory or Parameter mode.', 'langforge'),
                                    '<code>' . esc_html($lf_host) . '</code>'
                                ); ?>
                            </div>
                            <?php endif; ?>
                            <div class="lf-domain-map__grid" style="display:grid;grid-template-columns:160px 1fr;gap:8px 12px;align-items:center;">
                                <?php foreach ($active_languages as $code => $lang):
                                    $val = isset($domain_map[$code]) ? $domain_map[$code] : '';
                                ?>
                                <label style="display:flex;align-items:center;gap:8px;font-weight:600;font-size:14px;">
                                    <span style="color:#0EC49E;font-weight:700;text-transform:uppercase;font-size:14px;letter-spacing:0.4px;"><?php echo esc_html($code); ?></span>
                                    <span style="color:#64748b;font-weight:500;"><?php echo esc_html($lang['native_name'] ?? $code); ?></span>
                                </label>
                                <input type="text" name="lf_domain_map[<?php echo esc_attr($code); ?>]" value="<?php echo esc_attr($val); ?>" placeholder="example.<?php echo esc_attr($code); ?>" class="regular-text" style="font-family:'JetBrains Mono',ui-monospace,monospace;font-size:14px;">
                                <?php endforeach; ?>
                            </div>
                            <p class="lf-hint" style="margin:8px 0 0;font-size:14px;"><?php esc_html_e('Tip: if you also use www, list both (one in each language). Empty rows are ignored.', 'langforge'); ?></p>
                        </div>
                        <script>
                        // Show/hide the domain map block as the user toggles URL mode.
                        jQuery(function($){
                            $('input[name="lf_url_format"]').on('change', function(){
                                $('.lf-domain-map').toggle(this.value === 'domain');
                            });
                        });
                        </script>

                        <div class="lf-option-row">
                            <label class="lf-toggle-label">
                                <input type="checkbox" name="lf_hide_default_language" value="1" <?php checked($hide_default); ?>>
                                <span class="lf-toggle-text">
                                    <strong><?php esc_html_e('Hide default language in URL', 'langforge'); ?></strong>
                                    <span><?php esc_html_e('Use example.com/page instead of example.com/ru/page for default language', 'langforge'); ?></span>
                                </span>
                            </label>
                        </div>

                        <div class="lf-option-row">
                            <label class="lf-toggle-label">
                                <input type="checkbox" name="lf_auto_detect_language" value="1" <?php checked(get_option('lf_auto_detect_language', false)); ?>>
                                <span class="lf-toggle-text">
                                    <strong><?php esc_html_e('Auto-detect browser language', 'langforge'); ?></strong>
                                    <span><?php esc_html_e('Redirect first-time visitors to their browser language if available', 'langforge'); ?></span>
                                </span>
                            </label>
                        </div>
                    </div>
                </div>

                <!-- ==================== POST TYPES + NOTIFICATIONS (side by side) ==================== -->
                <div class="lf-card">
                    <div class="lf-card-head">
                        <span class="dashicons dashicons-admin-post"></span>
                        <h2><?php esc_html_e('Content & Notifications', 'langforge'); ?><?php Forge_Docs_Link::render('langforge', 'translating-a-post-or-page', __('Translatable content', 'langforge'), __('Choose which post types are translatable and set fallback rules.', 'langforge')); ?></h2>
                    </div>
                    <div class="lf-card-body">
                        <h3 class="lf-sub-heading"><?php esc_html_e('Translatable Post Types', 'langforge'); ?></h3>
                        <?php
                        $all_post_types = get_post_types(['public' => true], 'objects');
                        $saved_types = get_option('lf_translatable_post_types', []);
                        ?>
                        <div class="lf-pt-grid">
                            <?php foreach ($all_post_types as $pt):
                                if ($pt->name === 'attachment') continue;
                                $checked = in_array($pt->name, $saved_types) || empty($saved_types);
                            ?>
                            <label class="lf-pt-item <?php echo $checked ? 'active' : ''; ?>">
                                <input type="checkbox" name="lf_post_types[]" value="<?php echo esc_attr($pt->name); ?>" <?php checked($checked); ?>>
                                <span class="lf-pt-name"><?php echo esc_html($pt->labels->name); ?></span>
                                <code><?php echo esc_html($pt->name); ?></code>
                            </label>
                            <?php endforeach; ?>
                        </div>

                        <?php
                        $fallbacks = get_option('lf_language_fallbacks', []);
                        // master switch for the fallback chain. When off,
                        // get_translation stops after the direct lookup and returns
                        // null if no translation exists for the requested language —
                        // no silent substitution from another language.
                        $fallbacks_enabled = (bool) get_option('lf_fallbacks_enabled', true);
                        $has_non_default = false;
                        foreach ($active_languages as $code => $lang) {
                            if ($code !== $default_language) { $has_non_default = true; break; }
                        }
                        if ($has_non_default): ?>
                        <h3 class="lf-sub-heading" style="margin-top:24px;"><?php esc_html_e('Language Fallback Chain', 'langforge'); ?></h3>
                        <div class="lf-option-row" style="margin:8px 0 12px;">
                            <label class="lf-toggle-label">
                                <input type="checkbox" name="lf_fallbacks_enabled" value="1" <?php checked($fallbacks_enabled); ?> id="lf-fallbacks-enabled-toggle">
                                <span class="lf-toggle-text">
                                    <strong><?php esc_html_e('Use fallback languages', 'langforge'); ?></strong>
                                    <span><?php esc_html_e('When a translation is missing, try these languages in order. Turn off to show no content for missing translations (strict mode).', 'langforge'); ?></span>
                                </span>
                            </label>
                        </div>
                        <div id="lf-fallback-chain-wrap" style="<?php echo $fallbacks_enabled ? '' : 'opacity:0.45;pointer-events:none;'; ?>">
                        <p class="lf-hint"><?php esc_html_e('When a translation is not available, try these languages in order.', 'langforge'); ?></p>
                        <?php foreach ($active_languages as $code => $lang):
                            if ($code === $default_language) continue; ?>
                        <div class="lf-fallback-row">
                            <span class="lf-flag"><?php echo esc_html(self::get_flag_emoji($all_languages[$code]['flag'] ?? $code)); ?></span>
                            <strong><?php echo esc_html($lang['native_name'] ?? $code); ?></strong>
                            <span class="lf-fallback-arrow">&rarr;</span>
                            <input type="text" name="lf_fallbacks[<?php echo esc_attr($code); ?>]"
                                   value="<?php echo esc_attr(isset($fallbacks[$code]) ? implode(',', $fallbacks[$code]) : $default_language); ?>"
                                   class="lf-inp-sm" style="width:120px;"
                                   placeholder="<?php echo esc_attr($default_language); ?>">
                        </div>
                        <?php endforeach; ?>
                        </div>
                        <script>
                        (function(){
                            var toggle = document.getElementById('lf-fallbacks-enabled-toggle');
                            var wrap   = document.getElementById('lf-fallback-chain-wrap');
                            if (!toggle || !wrap) return;
                            toggle.addEventListener('change', function(){
                                wrap.style.opacity       = toggle.checked ? '' : '0.45';
                                wrap.style.pointerEvents = toggle.checked ? '' : 'none';
                            });
                        })();
                        </script>
                        <?php endif; ?>

                        <div class="lf-option-row" style="margin-top:24px;">
                            <label class="lf-toggle-label">
                                <input type="checkbox" name="lf_email_notifications" value="1" <?php checked(get_option('lf_email_notifications', false)); ?>>
                                <span class="lf-toggle-text">
                                    <strong><?php esc_html_e('Email Notifications', 'langforge'); ?></strong>
                                    <span><?php esc_html_e('Notify translators when new content needs translation', 'langforge'); ?></span>
                                </span>
                            </label>
                        </div>
                    </div>
                </div>

                <!-- ==================== AI TRANSLATION CARD ==================== -->
                <div class="lf-card">
                    <div class="lf-card-head">
                        <span class="dashicons dashicons-translation"></span>
                        <h2><?php esc_html_e('AI Translation', 'langforge'); ?><?php Forge_Docs_Link::render('langforge', 'using-ai-to-translate-pro', __('AI translation', 'langforge'), __('One-click translation via Forge AI — no API keys needed (PRO).', 'langforge')); ?></h2>
                    </div>
                    <div class="lf-card-body">
                        <?php if (LF_License::instance()->can('ai_translation')): ?>
                            <div style="display:flex;align-items:center;gap:8px;font-size:14px;color:#059669;">
                                <span class="dashicons dashicons-yes-alt" style="font-size:20px;width:20px;height:20px;color:#059669;"></span>
                                <?php esc_html_e('AI translation is active. Powered by Forge AI (ChatGPT 4o mini) — no external API keys needed.', 'langforge'); ?>
                            </div>
                        <?php else: ?>
                            <div style="font-size:14px;color:#64748b;">
                                <?php esc_html_e('AI translation requires a Pro license. All AI processing is handled securely through the Forge API — ChatGPT 4o mini today, with additional engines on the roadmap.', 'langforge'); ?>
                                <a href="<?php echo esc_url(LF_License::upgrade_url()); ?>" target="_blank" style="color:#0EC49E;font-weight:600;margin-left:4px;"><?php esc_html_e('Upgrade to Pro', 'langforge'); ?></a>
                            </div>
                        <?php endif; ?>

                        <div class="lf-option-row" style="margin-top:12px;">
                            <label class="lf-toggle-label">
                                <input type="checkbox" name="lf_translate_comments" value="1" <?php checked(get_option('lf_translate_comments', false)); ?>>
                                <span class="lf-toggle-text">
                                    <strong><?php esc_html_e('Translate Comments', 'langforge'); ?></strong>
                                    <span><?php esc_html_e('Auto-translate visitor comments into the current site language', 'langforge'); ?></span>
                                </span>
                            </label>
                        </div>

                        <div class="lf-option-row" style="margin-top:16px;">
                            <label style="display:block;margin-bottom:6px;">
                                <strong><?php esc_html_e('String Auto-Registration Domains', 'langforge'); ?></strong>
                            </label>
                            <p class="description" style="margin:0 0 8px;">
                                <?php esc_html_e('Strings from these text domains will be auto-registered in String Translation when displayed on the frontend. One domain per line.', 'langforge'); ?>
                            </p>
                            <?php
                            $saved_domains = get_option('lf_auto_register_domains', '');
                            $theme_domain  = wp_get_theme()->get('TextDomain');
                            $placeholder   = $theme_domain ?: 'flavor';
                            ?>
                            <textarea name="lf_auto_register_domains" rows="3" style="width:100%;max-width:420px;font-family:monospace;font-size:14px;" placeholder="<?php echo esc_attr($placeholder); ?>"><?php echo esc_textarea($saved_domains); ?></textarea>
                            <p class="description" style="margin-top:4px;">
                                <?php echo wp_kses_post(sprintf(
                                    /* translators: %s: the current theme's text domain, wrapped in a code tag. */
                                    __('Current theme: %s. Leave empty to use theme domain by default.', 'langforge'),
                                    '<code>' . esc_html($theme_domain ?: '—') . '</code>'
                                )); ?>
                            </p>
                        </div>
                    </div>
                </div>

                <!-- ==================== TRANSLATION MODEL CARD ==================== -->
                <div class="lf-card">
                    <div class="lf-card-head">
                        <span class="dashicons dashicons-admin-network"></span>
                        <h2><?php esc_html_e('Translation Model', 'langforge'); ?></h2>
                    </div>
                    <div class="lf-card-body">
                        <?php
                        // Model switcher hidden for now (2026-04-20). Only one
                        // model is available — we still write the option so
                        // forge-api receives the expected `model` field.
                        // History of the radio group is in git; restore when
                        // additional models ship.
                        ?>
                        <input type="hidden" name="lf_translation_model" value="openai-mini">

                        <div style="display:flex;gap:12px;align-items:flex-start;padding:14px 16px;border:1px solid #e2e8f0;border-radius:10px;background:linear-gradient(135deg,rgba(14,196,158,0.06),rgba(14,196,158,0.01));">
                            <span class="dashicons dashicons-info" style="color:#0EC49E;font-size:20px;width:20px;height:20px;margin-top:1px;"></span>
                            <div style="flex:1;">
                                <p style="margin:0 0 6px;font-size:14px;font-weight:600;color:#0f172a;">
                                    <?php esc_html_e('Currently using ChatGPT 4o mini', 'langforge'); ?>
                                </p>
                                <p style="margin:0;font-size:14px;color:#475569;line-height:1.5;">
                                    <?php esc_html_e('All translations run through a fast, cost-efficient OpenAI model on our server — no API keys to manage on your side.', 'langforge'); ?>
                                </p>
                                <p style="margin:10px 0 0;font-size:14px;color:#94a3b8;line-height:1.5;">
                                    <?php esc_html_e('Support for additional translation engines is on the roadmap. When ready, you will be able to pick between models with different credit costs per translation.', 'langforge'); ?>
                                </p>
                            </div>
                        </div>

                        <p class="lf-hint" style="margin:14px 0 0;font-size:14px;color:#94a3b8;">
                            <?php esc_html_e('Credit balance and per-plugin breakdown live on your Avakode dashboard under Credits.', 'langforge'); ?>
                        </p>
                    </div>
                </div>

                </div><!-- .lf-settings-grid -->

                <!-- ==================== LANGUAGE SWITCHER CARD ==================== -->
                <div class="lf-card" style="margin-top:20px;">
                    <div class="lf-card-head">
                        <span class="dashicons dashicons-translation"></span>
                        <h2><?php esc_html_e('Language Switcher', 'langforge'); ?><?php Forge_Docs_Link::render('langforge', 'adding-a-language-switcher-to-your-site', __('Language switcher', 'langforge'), __('Widget, shortcode, menu item — pick a placement.', 'langforge')); ?></h2>
                    </div>
                    <div class="lf-card-body">
                        <p style="color:#64748b;font-size:14px;margin:0 0 16px;">
                            <?php esc_html_e('These settings apply to the lf_language_switcher() function and shortcode used in your theme.', 'langforge'); ?>
                        </p>

                        <div class="lf-option-row">
                            <label for="lf_switcher_style"><strong><?php esc_html_e('Style:', 'langforge'); ?></strong></label>
                            <select name="lf_switcher_style" id="lf_switcher_style" style="margin-left:8px;">
                                <option value="dropdown" <?php selected(get_option('lf_switcher_style', 'dropdown'), 'dropdown'); ?>><?php esc_html_e('Dropdown', 'langforge'); ?></option>
                                <option value="list" <?php selected(get_option('lf_switcher_style', 'dropdown'), 'list'); ?>><?php esc_html_e('List', 'langforge'); ?></option>
                                <option value="flags" <?php selected(get_option('lf_switcher_style', 'dropdown'), 'flags'); ?>><?php esc_html_e('Flags only', 'langforge'); ?></option>
                                <option value="plain" <?php selected(get_option('lf_switcher_style', 'dropdown'), 'plain'); ?>><?php esc_html_e('Plain (unstyled)', 'langforge'); ?></option>
                            </select>
                        </div>

                        <div class="lf-option-row" style="margin-top:12px;">
                            <label class="lf-toggle-label">
                                <input type="checkbox" name="lf_switcher_show_flags" value="1" <?php checked(get_option('lf_switcher_show_flags', '1')); ?>>
                                <span class="lf-toggle-text"><?php esc_html_e('Show flags', 'langforge'); ?></span>
                            </label>
                        </div>
                        <div class="lf-option-row" style="margin-top:8px;">
                            <label class="lf-toggle-label">
                                <input type="checkbox" name="lf_switcher_show_names" value="1" <?php checked(get_option('lf_switcher_show_names', '1')); ?>>
                                <span class="lf-toggle-text"><?php esc_html_e('Show language names', 'langforge'); ?></span>
                            </label>
                        </div>
                        <div class="lf-option-row" style="margin-top:8px;">
                            <label class="lf-toggle-label">
                                <input type="checkbox" name="lf_switcher_show_current" value="1" <?php checked(get_option('lf_switcher_show_current', '1')); ?>>
                                <span class="lf-toggle-text"><?php esc_html_e('Show current language in list', 'langforge'); ?></span>
                            </label>
                            <p class="lf-hint" style="margin:4px 0 0 22px;font-size:14px;color:#94a3b8;"><?php esc_html_e('Applies to List / Flags / Plain styles. The Dropdown style always shows the current language as the trigger chip — that is the select control itself, not a list item.', 'langforge'); ?></p>
                        </div>
                        <div class="lf-option-row" style="margin-top:8px;">
                            <label class="lf-toggle-label">
                                <input type="checkbox" name="lf_switcher_hide_empty" value="1" <?php checked(get_option('lf_switcher_hide_empty', '0'), '1'); ?>>
                                <span class="lf-toggle-text"><?php esc_html_e('Hide languages with no translations', 'langforge'); ?></span>
                            </label>
                            <p class="lf-hint" style="margin:4px 0 0 22px;font-size:14px;color:#94a3b8;"><?php esc_html_e('Skip languages from the public switcher when they have zero published translated posts/pages. The default language always stays visible.', 'langforge'); ?></p>
                        </div>
                    </div>
                </div>
                <?php endif; ?>

                <div class="lf-settings-actions">
                    <input type="submit" name="lf_save_settings" class="lf-save-btn" value="<?php esc_attr_e('Save Settings', 'langforge'); ?>">
                </div>
                <div class="lf-save-bar" id="lf-settings-savebar" role="status" aria-live="polite">
                    <span class="lf-savebar-copy"><?php esc_html_e('You have unsaved settings changes.', 'langforge'); ?></span>
                    <button type="button" class="button" id="lf-settings-reset"><?php esc_html_e('Reset', 'langforge'); ?></button>
                    <input type="submit" name="lf_save_settings" class="lf-save-btn lf-save-btn--sticky" value="<?php esc_attr_e('Save Settings', 'langforge'); ?>">
                </div>
            </form>
            <script>
            // Settings dirty-state guard (mirrors Form Forge): a floating bar with
            // Reset + Save appears only when the form differs from its saved state.
            // Reset reloads the saved state; navigating away while dirty warns.
            (function(){
                var form  = document.getElementById('lf-settings-form');
                var bar   = document.getElementById('lf-settings-savebar');
                var reset = document.getElementById('lf-settings-reset');
                if (!form || !bar) return;
                var skip = { lf_settings_nonce: 1, _wp_http_referer: 1 };
                var leaveMessage = <?php echo wp_json_encode(__('You have unsaved settings changes. Leave this page?', 'langforge')); ?>;
                function snapshot(){
                    var pairs = [];
                    new FormData(form).forEach(function(value, key){
                        if (skip[key]) return;
                        pairs.push(key + '=' + String(value));
                    });
                    return pairs.sort().join('&');
                }
                var initialSnapshot = snapshot();
                var dirty = false;
                function setDirty(next){ dirty = !!next; bar.classList.toggle('is-visible', dirty); }
                function recompute(){ setDirty(snapshot() !== initialSnapshot); }
                form.addEventListener('input', recompute);
                form.addEventListener('change', recompute);
                // The active-languages table adds/removes rows via JS (not input
                // events), so watch it directly to keep the dirty state honest.
                var tbody = document.getElementById('lf-active-languages-body');
                if (tbody && window.MutationObserver) {
                    new MutationObserver(recompute).observe(tbody, { childList: true, subtree: true });
                }
                form.addEventListener('submit', function(){ setDirty(false); });
                if (reset) {
                    reset.addEventListener('click', function(){
                        setDirty(false);            // suppress the unload guard for this intentional revert
                        window.location.reload();   // restore the saved state (incl. the language table)
                    });
                }
                window.addEventListener('beforeunload', function(e){
                    if (!dirty) return;
                    e.preventDefault();
                    e.returnValue = '';
                });
                document.addEventListener('click', function(e){
                    if (!dirty) return;
                    var link = e.target.closest ? e.target.closest('a[href]') : null;
                    if (!link || link.target === '_blank') return;
                    if (link.href.indexOf('#') === link.href.length - 1) return;
                    if (!window.confirm(leaveMessage)) { e.preventDefault(); return; }
                    setDirty(false);
                });
            })();
            </script>

            <style>
            .lf-settings-wrap{max-width:860px;}
            .lf-settings-header{margin-bottom:24px;padding-bottom:16px;border-bottom:2px solid var(--avl-primary);}
            .lf-settings-header h1{display:flex;align-items:center;font-size:22px;font-weight:700;margin:0;}
            .lf-settings-header .dashicons{color:var(--avl-primary);font-size:24px;width:24px;height:24px;}
            .lf-settings-grid{display:flex;flex-direction:column;gap:20px;}

            /* Cards */
            .lf-card{background:var(--avl-bg-white);border:1px solid var(--avl-border);border-radius:12px;box-shadow:var(--avl-shadow);overflow:visible;}
            .lf-card-head{display:flex;align-items:center;gap:10px;padding:16px 20px;border-bottom:1px solid var(--avl-border);background:var(--avl-bg);border-radius:12px 12px 0 0;}
            .lf-card-head .dashicons{color:var(--avl-primary);font-size:18px;width:18px;height:18px;}
            .lf-card-head h2{margin:0;font-size:15px;font-weight:600;}
            .lf-card-body{padding:20px;}
            .lf-card-body--flush{padding:0;}

            /* Language Table */
            .lf-lang-table{width:100%;border-collapse:collapse;}
            .lf-lang-table thead th{padding:10px 14px;font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:var(--avl-text-secondary);border-bottom:2px solid var(--avl-border);background:var(--avl-bg);text-align:left;}
            .lf-lang-table tbody tr{transition:all 0.2s;border-bottom:1px solid #f1f5f9;}
            .lf-lang-table tbody tr:last-child{border-bottom:none;}
            .lf-lang-table tbody tr:hover{background:#fafafe;}
            .lf-lang-table td{padding:12px 14px;vertical-align:middle;}
            .lf-col-sm{width:70px;}.lf-col-center{width:80px;text-align:center!important;}.lf-col-xs{width:44px;}
            .lf-flag{font-size:20px;vertical-align:middle;margin-right:8px;}
            .lf-english{color:var(--avl-text-secondary);font-size:14px;margin-left:4px;}
            .lf-rtl-badge{display:inline-block;margin-left:8px;padding:1px 6px;font-size:14px;font-weight:700;letter-spacing:.04em;color:#fff;background:linear-gradient(135deg,#7c3aed,#a855f7);border-radius:4px;vertical-align:middle;cursor:help;}
            .lf-lang-table code{background:var(--avl-bg);padding:2px 8px;border-radius:4px;font-size:14px;color:var(--avl-primary);font-weight:600;}
            .lf-inp-sm{width:72px!important;padding:5px 8px!important;border:1px solid var(--avl-border)!important;border-radius:6px!important;font-size:14px!important;transition:border-color 0.15s!important;}
            .lf-inp-sm:focus{border-color:var(--avl-primary)!important;outline:none!important;box-shadow:0 0 0 2px rgba(14,196,158,0.1)!important;}
            .lf-empty{text-align:center;padding:32px!important;color:var(--avl-text-secondary);font-size:14px;}

            /* Remove btn */
            .lf-btn-remove{display:inline-flex;align-items:center;justify-content:center;background:var(--avl-bg);border:1px solid var(--avl-border);color:#94a3b8;cursor:pointer;padding:0;width:30px;height:30px;border-radius:8px;transition:all 0.15s;}
            .lf-btn-remove .dashicons{font-size:18px;width:18px;height:18px;}
            .lf-btn-remove:hover{background:var(--avl-danger-light);border-color:var(--avl-danger);color:var(--avl-danger);}

            /* Add language */
            .lf-add-wrap{padding:12px 14px;border-top:1px solid var(--avl-border);background:var(--avl-bg);}
            .lf-add-inner{position:relative;max-width:340px;}
            .lf-add-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--avl-primary);font-size:18px;pointer-events:none;}
            .lf-add-inner input#lf-add-language-input{width:100%;padding:9px 12px 9px 36px!important;border:1px solid var(--avl-border)!important;border-radius:8px!important;font-size:14px!important;background:var(--avl-bg-white)!important;transition:all 0.15s;}
            .lf-add-inner input:focus{border-color:var(--avl-primary)!important;outline:none!important;box-shadow:0 0 0 3px rgba(14,196,158,0.1)!important;}
            .lf-dropdown{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--avl-bg-white);border:1px solid var(--avl-border);border-radius:10px;box-shadow:var(--avl-shadow-md);z-index:10000;max-height:280px;overflow-y:auto;opacity:0;transform:translateY(-4px);pointer-events:none;transition:opacity 0.15s,transform 0.15s;}
            .lf-dropdown.visible{opacity:1;transform:translateY(0);pointer-events:auto;}
            .lf-lang-option{display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;transition:background 0.1s;border-bottom:1px solid #f1f5f9;font-size:14px;}
            .lf-lang-option:last-child{border-bottom:none;}
            .lf-lang-option:hover{background:var(--avl-primary-light);}
            .lf-opt-flag{font-size:18px;flex-shrink:0;}
            .lf-opt-name{flex:1;min-width:0;}.lf-opt-name span{color:var(--avl-text-secondary);font-size:14px;}
            .lf-lang-option code{background:var(--avl-bg);padding:2px 8px;border-radius:4px;font-size:14px;color:var(--avl-primary);font-weight:600;flex-shrink:0;}
            .lf-lang-no-results{color:var(--avl-text-secondary);cursor:default;font-style:italic;justify-content:center;}
            .lf-lang-no-results:hover{background:none;}
            .lf-lang-added{animation:lf-row-in 0.35s ease;}
            @keyframes lf-row-in{from{background:var(--avl-primary-light);opacity:0.5}to{background:transparent;opacity:1}}

            /* URL Options */
            .lf-url-options{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:20px;}
            /* Specificity: .lf-admin .wrap label { gap:4px } applies globally
 from admin.css — chain .wrap to win on specificity. */
            .lf-admin .wrap label.lf-url-opt{display:flex;align-items:center;gap:12px;padding:14px 16px;border:2px solid var(--avl-border);border-radius:10px;cursor:pointer;transition:all 0.15s;background:var(--avl-bg-white);min-height:56px;}
            .lf-admin .wrap label.lf-url-opt:hover{border-color:#99F6E0;background:#fafafe;}
            .lf-admin .wrap label.lf-url-opt.active{border-color:var(--avl-primary);background:var(--avl-primary-light);}
            .lf-admin .wrap label.lf-url-opt input{margin:0;flex-shrink:0;accent-color:var(--avl-primary);}
            .lf-admin .wrap label.lf-url-opt .dashicons{color:var(--avl-text-secondary);margin:0;font-size:18px;width:18px;height:18px;flex-shrink:0;}
            .lf-admin .wrap label.lf-url-opt.active .dashicons{color:var(--avl-primary);}
            .lf-url-opt-text{display:flex;flex-direction:column;gap:2px;line-height:1.35;}
            .lf-url-opt-text strong{font-size:14px;color:var(--avl-text);}
            .lf-url-opt-text span{font-size:14px;color:var(--avl-text-secondary);}
            .lf-url-opt-text b{color:var(--avl-primary);font-weight:700;}

            /* Toggle options */
            .lf-option-row{padding:14px 0;border-top:1px solid #f1f5f9;}
            .lf-option-row:first-of-type{border-top:0;padding-top:6px;}
            .lf-admin .wrap label.lf-toggle-label{display:flex;align-items:center;gap:12px;cursor:pointer;}
            .lf-admin .wrap label.lf-toggle-label input{margin:0;flex-shrink:0;width:16px;height:16px;accent-color:var(--avl-primary);}
            .lf-toggle-text{display:flex;flex-direction:column;gap:2px;line-height:1.35;}
            .lf-toggle-text strong{font-size:14px;color:var(--avl-text);}
            .lf-toggle-text span{font-size:14px;color:var(--avl-text-secondary);}

            /* Sub headings */
            .lf-sub-heading{font-size:14px;font-weight:600;color:var(--avl-text);margin:0 0 10px;padding-bottom:8px;border-bottom:1px solid var(--avl-border);}
            .lf-hint{font-size:14px;color:var(--avl-text-secondary);margin:0 0 12px;}

            /* Post types grid */
            .lf-pt-grid{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px;}
            .lf-pt-item{display:inline-flex;align-items:center;gap:8px;padding:8px 14px;border:1px solid var(--avl-border);border-radius:10px;cursor:pointer;transition:all 0.15s;font-size:14px;background:var(--avl-bg-white);line-height:1.2;}
            .lf-pt-item:hover{border-color:#99F6E0;background:#fafafe;}
            .lf-pt-item.active{border-color:var(--avl-primary);background:var(--avl-primary-light);}
            .lf-pt-item input{margin:0;accent-color:var(--avl-primary);width:14px;height:14px;}
            .lf-pt-item code{font-size:14px;color:var(--avl-text-secondary);background:none;padding:0;}
            .lf-pt-name{font-weight:500;}
            .lf-lang-locked{background:#f8fafc;color:#64748b;}
            .lf-lang-locked .lf-flag,
            .lf-lang-locked strong,
            .lf-lang-locked .lf-english,
            .lf-lang-locked code{opacity:.72;}
            .lf-locked-value{font-size:13px;color:#64748b;}
            .lf-upgrade-inline{font-size:13px;font-weight:600;color:#0f766e;text-decoration:none;}
            .lf-upgrade-inline:hover{text-decoration:underline;}

            /* Fallback */
            .lf-fallback-row{display:flex;align-items:center;gap:10px;padding:8px 0;font-size:14px;}
            .lf-fallback-arrow{color:var(--avl-text-secondary);font-size:16px;}

            /* Tool buttons */
            .lf-tool-btn{display:inline-flex;align-items:center;gap:6px;padding:9px 16px;background:var(--avl-bg-white);border:1px solid var(--avl-border);border-radius:8px;font-size:14px;font-weight:500;color:var(--avl-text);cursor:pointer;transition:all 0.15s;text-decoration:none;}
            .lf-tool-btn:hover{border-color:var(--avl-primary);color:var(--avl-primary);background:var(--avl-primary-light);}
            .lf-tool-btn .dashicons{font-size:16px;width:16px;height:16px;color:var(--avl-primary);}

            /* Selects & Inputs in cards */
            .lf-card select,.lf-settings-wrap select,.lf-card-input,.lf-card input[type=password]{width:100%!important;padding:9px 12px!important;border:1px solid var(--avl-border)!important;border-radius:8px!important;font-size:14px!important;transition:border-color 0.15s!important;background-color:var(--avl-bg-white)!important;box-sizing:border-box!important;height:auto!important;line-height:1.4!important;}
            .lf-card select,.lf-settings-wrap select{padding-right:32px!important;-webkit-appearance:none!important;appearance:none!important;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M2 4l4 4 4-4'/%3E%3C/svg%3E")!important;background-repeat:no-repeat!important;background-position:right 10px center!important;}
            .lf-card-input:focus,.lf-card select:focus,.lf-card input:focus{border-color:var(--avl-primary)!important;outline:none!important;box-shadow:0 0 0 3px rgba(14,196,158,0.1)!important;}

            /* Save bar */
            .lf-settings-actions{margin-top:24px;}
            .lf-save-btn{display:inline-flex;align-items:center;justify-content:center;background:var(--avl-primary);color:#0C1A3A;border:none;border-radius:10px;padding:12px 32px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.2s;box-shadow:0 2px 8px rgba(14,196,158,0.2);}
            .lf-save-btn:hover{background:var(--avl-primary-hover);transform:translateY(-1px);box-shadow:0 4px 16px rgba(14,196,158,0.3);}
            /* Floating "unsaved changes" bar — appears only when the form is dirty
 (mirrors Form Forge, in Lang Forge's teal palette). */
            .lf-save-bar{position:fixed;left:180px;right:20px;bottom:20px;max-width:860px;z-index:10000;display:none;align-items:center;justify-content:flex-end;gap:12px;padding:12px 14px;background:#fff;border:1px solid rgba(14,196,158,0.45);border-radius:12px;box-shadow:0 12px 32px rgba(15,23,42,0.16);}
            .lf-save-bar.is-visible{display:flex;}
            .lf-savebar-copy{margin-right:auto;color:#0f766e;font-weight:600;font-size:14px;}
            .lf-save-btn--sticky{padding:9px 18px;border-radius:8px;box-shadow:none;}
            .lf-save-bar #lf-settings-reset{margin:0;border-radius:8px;}
            body.folded .lf-save-bar{left:56px;}
            @media(max-width:960px){.lf-save-bar{left:36px;right:20px;}}
            @media(max-width:782px){.lf-save-bar{left:12px;right:12px;bottom:12px;flex-wrap:wrap;align-items:stretch;}.lf-savebar-copy{width:100%;margin-right:0;}.lf-save-bar .button,.lf-save-bar .lf-save-btn{flex:1;}}
            </style>

            <script>
            (function(){
	                var allLangs=<?php echo wp_json_encode($all_languages); ?>;
	                var flagMap=<?php echo wp_json_encode(self::get_flag_map()); ?>;
	                var activeSet={};
	                document.querySelectorAll('#lf-active-languages-body tr[data-code]').forEach(function(r){activeSet[r.dataset.code]=true;});
	                var input=document.getElementById('lf-add-language-input'),dd=document.getElementById('lf-language-dropdown'),tbody=document.getElementById('lf-active-languages-body');
	                if (input && dd && tbody) {

                function renderDD(q){q=(q||'').toLowerCase();var html='';for(var c in allLangs){if(activeSet[c])continue;var L=allLangs[c];if(q&&(L.native_name+' '+L.name+' '+c).toLowerCase().indexOf(q)===-1)continue;var f=flagMap[L.flag||c]||'';html+='<div class="lf-lang-option" data-code="'+c+'"><span class="lf-opt-flag">'+f+'</span><span class="lf-opt-name"><strong>'+L.native_name+'</strong> <span>('+L.name+')</span></span><code>'+c+'</code></div>';}if(!html)html='<div class="lf-lang-option lf-lang-no-results">No matching languages</div>';dd.innerHTML=html;dd.classList.add('visible');}
                function hideDD(){dd.classList.remove('visible');}
                input.addEventListener('focus',function(){renderDD(this.value);});
                input.addEventListener('input',function(){renderDD(this.value);});
                document.addEventListener('click',function(e){if(!e.target.closest('.lf-add-inner'))hideDD();});
                dd.addEventListener('click',function(e){var o=e.target.closest('.lf-lang-option[data-code]');if(!o)return;addLang(o.dataset.code);input.value='';hideDD();input.focus();});

                function addLang(c){if(activeSet[c])return;activeSet[c]=true;var L=allLangs[c],f=flagMap[L.flag||c]||'';var noR=document.getElementById('lf-no-languages-row');if(noR)noR.remove();var tr=document.createElement('tr');tr.dataset.code=c;tr.className='lf-lang-added';tr.innerHTML='<td><input type="hidden" name="lf_languages[]" value="'+c+'"><span class="lf-flag">'+f+'</span><strong>'+L.native_name+'</strong> <span class="lf-english">'+L.name+'</span></td><td><code>'+c+'</code></td><td><input type="text" name="lf_display_name['+c+']" value="'+c+'" class="lf-inp-sm" placeholder="'+c+'"></td><td class="lf-col-center"><input type="checkbox" name="lf_show_in_switcher['+c+']" value="1" checked></td><td class="lf-col-center"><input type="radio" name="lf_default_language" value="'+c+'"></td><td class="lf-col-center"><button type="button" class="lf-btn-remove" data-code="'+c+'"><span class="dashicons dashicons-no-alt"></span></button></td>';tbody.appendChild(tr);setTimeout(function(){tr.classList.remove('lf-lang-added');},400);}

	                tbody.addEventListener('click',function(e){var btn=e.target.closest('.lf-btn-remove');if(!btn)return;var c=btn.dataset.code,row=btn.closest('tr');if(row.querySelector('input[type=radio]:checked')){alert('Cannot remove the default language.');return;}row.style.opacity='0';row.style.transform='translateX(20px)';setTimeout(function(){row.remove();delete activeSet[c];if(!tbody.querySelector('tr[data-code]'))tbody.innerHTML='<tr id="lf-no-languages-row"><td colspan="6" class="lf-empty">No languages added yet.</td></tr>';},200);});
	                }

	                /* URL option cards toggle */
                document.querySelectorAll('.lf-url-opt input[type=radio]').forEach(function(r){r.addEventListener('change',function(){document.querySelectorAll('.lf-url-opt').forEach(function(o){o.classList.toggle('active',o.querySelector('input').checked);});});});
                /* PT items toggle */
                document.querySelectorAll('.lf-pt-item input').forEach(function(cb){cb.addEventListener('change',function(){this.closest('.lf-pt-item').classList.toggle('active',this.checked);});});
            })();
            </script>
            
	            <?php if (false): ?>
	            <!-- Legacy Tools Card: moved to lf-tools-hub in . -->
	            <div class="lf-card" style="margin-top:20px;">
                <div class="lf-card-head">
                    <span class="dashicons dashicons-admin-tools"></span>
                    <h2><?php esc_html_e('Tools', 'langforge'); ?><?php Forge_Docs_Link::render('langforge', 'first-time-setup', __('Tools', 'langforge'), __('Flush rewrite rules, populate TM, import WPML data.', 'langforge')); ?></h2>
                </div>
                <div class="lf-card-body">
                    <div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;">
                        <a href="<?php echo esc_url(admin_url('admin.php?page=lf-settings&action=flush_rules')); ?>" class="lf-tool-btn">
                            <span class="dashicons dashicons-update"></span>
                            <?php esc_html_e('Flush Rewrite Rules', 'langforge'); ?>
                        </a>
                        <?php if (LF_License::instance()->can('translation_memory')): ?>
                        <button type="button" class="lf-tool-btn" id="avl-populate-tm">
                            <span class="dashicons dashicons-database"></span>
                            <?php esc_html_e('Populate Translation Memory', 'langforge'); ?>
                        </button>
                        <?php else: ?>
                        <span class="lf-tool-btn" style="opacity:0.5;cursor:default;pointer-events:none;">
                            <span class="dashicons dashicons-database"></span>
                            <?php esc_html_e('Populate Translation Memory', 'langforge'); ?>
                            <span class="lf-pro-badge" style="font-size:14px;background:#0EC49E;color:#fff;padding:1px 5px;border-radius:3px;margin-left:4px;">PRO</span>
                        </span>
                        <?php endif; ?>
                        <?php if ($this->wpml_data_exists()): ?>
                        <a href="<?php echo esc_url(wp_nonce_url(admin_url('admin.php?page=lf-settings&action=migrate_wpml'), 'lf_migrate_wpml')); ?>" class="lf-tool-btn" onclick="return confirm('<?php esc_attr_e('Import all WPML translation relationships?', 'langforge'); ?>');">
                            <span class="dashicons dashicons-migrate"></span>
                            <?php esc_html_e('Import WPML Data', 'langforge'); ?>
                        </a>
                        <?php endif; ?>
                    </div>
                    <span id="avl-tm-result" style="display:block;margin-top:8px;font-size:14px;"></span>

                    <div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--avl-border);">
                        <?php if (LF_License::instance()->can('bulk_actions')): ?>
                            <?php LF_Bulk_Actions::render_duplicate_button(); ?>
                        <?php else: ?>
                            <h2 style="display:flex;align-items:center;gap:8px;">
                                <?php esc_html_e('One-Click Site Duplication', 'langforge'); ?>
                                <span class="lf-pro-badge" style="font-size:14px;background:#0EC49E;color:#fff;padding:2px 6px;border-radius:4px;">PRO</span>
                            </h2>
                            <p class="description"><?php esc_html_e('Create draft copies of all content for a new language.', 'langforge'); ?></p>
                            <div style="margin-top:10px;opacity:0.5;pointer-events:none;">
                                <select disabled><option><?php esc_html_e('Select language', 'langforge'); ?></option></select>
                                <button class="button" disabled><?php esc_html_e('Duplicate Only', 'langforge'); ?></button>
                            </div>
                            <p style="margin-top:8px;">
                                <a href="<?php echo esc_url(LF_License::upgrade_url()); ?>" style="color:#0EC49E;font-weight:600;"><?php esc_html_e('Upgrade to Pro', 'langforge'); ?> →</a>
                            </p>
                        <?php endif; ?>
                    </div>
                </div>
            </div>

            <script>
            jQuery('#avl-populate-tm').on('click', function() {
                var $btn = jQuery(this);
                $btn.prop('disabled', true).css('opacity','0.6');
                jQuery('#avl-tm-result').html('<span style="color:var(--avl-text-secondary);">Processing...</span>');
                jQuery.ajax({
                    url: '<?php echo esc_url(rest_url('langforge/v1/tm/populate')); ?>',
                    method: 'POST',
                    headers: { 'X-WP-Nonce': '<?php echo esc_html(wp_create_nonce('wp_rest')); ?>' },
                    success: function(r) {
                        $btn.prop('disabled', false).css('opacity','1');
                        jQuery('#avl-tm-result').html('<span style="color:var(--avl-success);">&#10003; ' + r.message + '</span>');
                    }
                });
	            });
	            </script>
	            <?php endif; ?>

        </div>
        <?php
        
        // Handle flush rules
        if (isset($_GET['action']) && $_GET['action'] === 'flush_rules') {
            flush_rewrite_rules();
            echo '<div class="notice notice-success"><p>' . esc_html__('Rewrite rules flushed.', 'langforge') . '</p></div>';
        }
        
        // Handle WPML migration
        if (isset($_GET['action']) && $_GET['action'] === 'migrate_wpml' && isset($_GET['_wpnonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'lf_migrate_wpml')) {
            $result = $this->migrate_wpml_data();
            if ($result['success']) {
                /* translators: %d: number of translation groups imported from WPML. */
                echo '<div class="notice notice-success"><p>' . sprintf(esc_html__('Migration complete! Imported %d translation groups.', 'langforge'), esc_html($result['count'])) . '</p></div>';
            } else {
                echo '<div class="notice notice-error"><p>' . esc_html__("Migration failed: ", "langforge") . esc_html($result['error']) . '</p></div>';
            }
        }
    }
    
    /**
 * Save settings
 */
    private function save_settings() {
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in render_settings_page (lf_settings_nonce / lf_save_settings) before this method runs.
        global $wpdb;
        $all_languages = LF_Core::instance()->get_available_languages();
        $context = isset($_POST['lf_settings_context'])
            ? sanitize_key(wp_unslash($_POST['lf_settings_context']))
            : 'all';
        $save_languages = ($context === 'all' || $context === 'languages');
        $save_settings  = ($context === 'all' || $context === 'settings');
        // Snapshot BEFORE updating — needed to diff for the "language
        // removed" confirmation prompt at the bottom of this method.
        $old_active    = get_option('lf_active_languages', []);
        $active = $old_active;
        $default = get_option('lf_default_language', 'ru');
        $hide_default = isset($_POST['lf_hide_default_language']);
        $url_format = isset($_POST['lf_url_format']) ? sanitize_text_field(wp_unslash($_POST['lf_url_format'])) : 'directory';

        if ($save_languages) {
            $selected = isset($_POST['lf_languages']) ? array_map('sanitize_text_field', wp_unslash($_POST['lf_languages'])) : [];
            $default = isset($_POST['lf_default_language']) ? sanitize_text_field(wp_unslash($_POST['lf_default_language'])) : $default;
            $display_names = isset($_POST['lf_display_name']) ? array_map('sanitize_text_field', wp_unslash($_POST['lf_display_name'])) : [];
            $show_in_switcher = isset($_POST['lf_show_in_switcher']) && is_array($_POST['lf_show_in_switcher']) ? wp_unslash($_POST['lf_show_in_switcher']) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Used only as isset flags (boolean presence) keyed by language code below.

            // Build active languages array
            $active = [];
            foreach ($selected as $code) {
                if (isset($all_languages[$code])) {
                    $active[$code] = $all_languages[$code];
                    // Add display name (custom text for switcher)
                    $active[$code]['display_name'] = isset($display_names[$code]) && !empty($display_names[$code])
                        ? $display_names[$code]
                        : $code;
                    // Add show in switcher flag
                    $active[$code]['show_in_switcher'] = isset($show_in_switcher[$code]);
                }
            }

            // Make sure default language participates in the limit decision.
            if (!isset($active[$default]) && isset($all_languages[$default])) {
                $active[$default] = $all_languages[$default];
                $active[$default]['display_name'] = isset($display_names[$default]) ? $display_names[$default] : $default;
                $active[$default]['show_in_switcher'] = isset($show_in_switcher[$default]);
            }

            // Check language limit before saving. Free keeps over-limit
            // languages as locked saved data, but they must not stay in the
            // editable/effective slice or a crafted POST can reactivate them.
            $count = count($active);
            if (!LF_License::instance()->check_limit('languages', $count)) {
                $limit = (int) LF_License::get_limit('languages');
                $trimmed = [];

                if ($limit > 0 && isset($active[$default])) {
                    $trimmed[$default] = $active[$default];
                }

                foreach ($active as $code => $data) {
                    if ($limit > 0 && count($trimmed) >= $limit) {
                        break;
                    }
                    if (!isset($trimmed[$code])) {
                        $trimmed[$code] = $data;
                    }
                }

                $active = $trimmed;
            }

            if (!LF_License::is_pro()) {
                $locked_existing = array_diff_key((array) $old_active, $active);
                if (!empty($locked_existing)) {
                    $active += $locked_existing;
                }
            }
        }
        
        if ($save_settings) {
            // Save translatable post types
            $post_types = isset($_POST['lf_post_types']) ? array_map('sanitize_text_field', wp_unslash($_POST['lf_post_types'])) : [];
            update_option('lf_translatable_post_types', $post_types);

            // Save language fallbacks
            $fallbacks = [];
            if (isset($_POST['lf_fallbacks']) && is_array($_POST['lf_fallbacks'])) {
                foreach (wp_unslash($_POST['lf_fallbacks']) as $lang => $chain) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Keys/values sanitized per-item below (sanitize_text_field).
                    $lang = sanitize_text_field($lang);
                    $chain = array_map('trim', explode(',', sanitize_text_field($chain)));
                    $chain = array_filter($chain);
                    if (!empty($chain)) {
                        $fallbacks[$lang] = $chain;
                    }
                }
            }
            update_option('lf_language_fallbacks', $fallbacks);

            // persist fallback master switch. Default = true so existing
            // sites keep current behaviour on upgrade.
            update_option('lf_fallbacks_enabled', isset($_POST['lf_fallbacks_enabled']));

            // Save notification setting
            $email_notifications = isset($_POST['lf_email_notifications']);
            update_option('lf_email_notifications', $email_notifications);

            // Save auto-detect language
            update_option('lf_auto_detect_language', isset($_POST['lf_auto_detect_language']));

            // Save comment translation toggle
            update_option('lf_translate_comments', isset($_POST['lf_translate_comments']));

            // Save auto-register domains
            if (isset($_POST['lf_auto_register_domains'])) {
                $domains = sanitize_textarea_field(wp_unslash($_POST['lf_auto_register_domains']));
                update_option('lf_auto_register_domains', $domains);
            }

        // Save per-language domain map (only relevant for Domain URL mode).
        // We always store it so switching mode back and forth doesn't lose the
        // map. Strip scheme if user pasted a URL, and drop empty rows.
            if (isset($_POST['lf_domain_map']) && is_array($_POST['lf_domain_map'])) {
                $clean_map = [];
                foreach (wp_unslash($_POST['lf_domain_map']) as $code => $host) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Keys/values sanitized per-item below (sanitize_key + host normalization).
                    $code = sanitize_key($code);
                    $host = trim((string) $host);
                    $host = preg_replace('#^https?://#i', '', $host);
                    $host = trim($host, "/ \t\n\r\0\x0B");
                    $host = strtolower($host);
                    if ($code !== '' && $host !== '') {
                        $clean_map[$code] = $host;
                    }
                }
                update_option('lf_domain_map', $clean_map);
            }

            // Save language switcher settings 
            if (isset($_POST['lf_switcher_style'])) {
                update_option('lf_switcher_style', sanitize_text_field(wp_unslash($_POST['lf_switcher_style'])));
            }
            update_option('lf_switcher_show_flags', isset($_POST['lf_switcher_show_flags']) ? '1' : '0');
            update_option('lf_switcher_show_names', isset($_POST['lf_switcher_show_names']) ? '1' : '0');
            update_option('lf_switcher_show_current', isset($_POST['lf_switcher_show_current']) ? '1' : '0');
            // Tester feedback 2026-04-18: hide languages with zero translated
            // content from the public switcher.
            update_option('lf_switcher_hide_empty', isset($_POST['lf_switcher_hide_empty']) ? '1' : '0');

            update_option('lf_hide_default_language', $hide_default);
            update_option('lf_url_format', $url_format);

            // Translation model — plugin just records the choice; forge-api
            // picks the actual OpenAI GPT backend (mini / gpt-4o) server-side.
            if (isset($_POST['lf_translation_model'])) {
                // Switcher hidden (2026-04-20) — only openai-mini is exposed. The
                // form always submits that value via a hidden input; anything else
                // coming in is force-corrected.
                update_option('lf_translation_model', 'openai-mini');
            }
        }

        if ($save_languages) {
            update_option('lf_active_languages', $active);
            update_option('lf_default_language', $default);
        }

        // refresh the in-memory Core snapshot so downstream renders on
        // the SAME request (One-Click Site Duplication dropdown, post-save
        // switcher, etc.) see the new language list immediately — without
        // this the user had to reload the browser to see a just-added language.
        if (method_exists(LF_Core::instance(), 'reload_settings')) {
            LF_Core::instance()->reload_settings();
        }

        if ($save_languages) {
            // Tester feedback 2026-04-18: removing a language from the active
            // list used to silently orphan posts that had _lf_language=that
            // code — they stayed published, the switcher wouldn't link to
            // them, and there was no UI hint. Mirror the WordPress user-delete
            // pattern: after the save, work out which languages just went
            // away, count their posts, and stash a pending decision so the
            // next admin render can ask "keep / draft / delete".
            $old_codes = array_keys((array) $old_active);
            $new_codes = array_keys($active);
            $removed   = array_diff($old_codes, $new_codes);
            $pending   = [];
            foreach ($removed as $code) {
                if ($code === $default) continue;
                $count = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- core table; admin read
                    "SELECT COUNT(*) FROM {$wpdb->postmeta} pm
                       JOIN {$wpdb->posts} p ON p.ID = pm.post_id
                      WHERE pm.meta_key = '_lf_language' AND pm.meta_value = %s
                        AND p.post_status IN ('publish','future','pending','private')",
                    $code
                ));
                if ($count > 0) {
                    $pending[$code] = [
                        'count' => $count,
                        'name'  => $old_active[$code]['native_name'] ?? $code,
                    ];
                }
            }
            if (!empty($pending)) {
                set_transient('lf_removed_lang_pending_' . get_current_user_id(), $pending, HOUR_IN_SECONDS);
            }
        }

        // Tester feedback 2026-04-18: a bunch of "I saved but nothing
        // changed on the site" reports turned out to be page-cache layers
        // (LiteSpeed on Hostinger, WP object cache, our own transients)
        // serving pre-save HTML. Purge every layer we know about so the
        // next visit actually reflects the new settings.
        if (method_exists(LF_Core::instance(), 'clear_empty_languages_cache')) {
            LF_Core::instance()->clear_empty_languages_cache();
        }
        wp_cache_flush();
        // LiteSpeed Cache (Hostinger's server-level cache uses this plugin
        // or the same purge hook). Safe no-op when the plugin isn't active.
        do_action('litespeed_purge_all'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional ACF/WPML/core compatibility
        // W3 Total Cache / WP Super Cache / WP Rocket hooks — all cheap
        // no-ops if those plugins aren't loaded.
        if (function_exists('w3tc_flush_all'))      w3tc_flush_all();
        if (function_exists('wp_cache_clear_cache')) wp_cache_clear_cache();
        if (function_exists('rocket_clean_domain')) rocket_clean_domain();

        // Flush rewrite rules
        flush_rewrite_rules();
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }

    /**
 * Get string counts per domain
 */
    private function get_domain_counts() {
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';
        
        $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; admin read
            "SELECT string_domain, COUNT(DISTINCT string_name) as count FROM $table GROUP BY string_domain" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix; static query.
        );
        
        $counts = [];
        foreach ($results as $row) {
            $counts[$row->string_domain] = $row->count;
        }
        return $counts;
    }
    
    /**
 * Get unique strings grouped by name with all translations
 */
    private function get_unique_strings($domain = null) {
        global $wpdb;
        $table = $wpdb->prefix . 'lf_strings';

        if ($domain) {
            $all_strings = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; admin read
                "SELECT * FROM $table WHERE string_domain = %s ORDER BY string_name, string_language", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, value is %-prepared.
                $domain
            ));
        } else {
            $all_strings = $wpdb->get_results( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; admin read
                "SELECT * FROM $table ORDER BY string_domain, string_name, string_language" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix; static query.
            );
        }
        
        // Build unique strings. For source_lang, use the record with the lowest id
        // (= the first language registered for this string name).
        $unique = [];
        foreach ($all_strings as $string) {
            $name = $string->string_name;
            if (!isset($unique[$name])) {
                $unique[$name] = [
                    'source_lang' => $string->string_language,
                    'source_id' => (int) $string->id,
                    'domain' => $string->string_domain,
                    'translations' => []
                ];
            } elseif ((int) $string->id < $unique[$name]['source_id']) {
                // Earlier record = real source
                $unique[$name]['source_lang'] = $string->string_language;
                $unique[$name]['source_id'] = (int) $string->id;
            }
            $unique[$name]['translations'][$string->string_language] = $string->string_value;
        }
        
        return $unique;
    }
    
    /**
 * Scan theme files for lf_e and lf__ calls and register strings
 */
    private function scan_theme_for_strings() {
        $theme_dir = get_template_directory();
        $files = $this->get_php_files($theme_dir);
        $count = 0;
        
        // Pattern to match lf_e('string', 'domain', 'lang') or lf__('string', 'domain', 'lang')
        $pattern = "/lf_e?\(\s*['\"]([^'\"]+)['\"]\s*(?:,\s*['\"]([^'\"]*)['\"])?\s*(?:,\s*['\"]([^'\"]+)['\"])?\s*\)/";
        
        foreach ($files as $file) {
            $content = file_get_contents($file);
            if (preg_match_all($pattern, $content, $matches, PREG_SET_ORDER)) {
                foreach ($matches as $match) {
                    $string = $match[1];
                    $domain = !empty($match[2]) ? $match[2] : 'default';
                    $source_lang = !empty($match[3]) ? $match[3] : LF_Core::instance()->get_default_language();
                    
                    LF_String_Translation::instance()->register_string($string, $string, $domain, $source_lang);
                    $count++;
                }
            }
        }
        
        return $count;
    }
    
    /**
 * Get all PHP files in directory recursively
 */
    private function get_php_files($dir) {
        $files = [];
        $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
        
        foreach ($iterator as $file) {
            if ($file->isFile() && $file->getExtension() === 'php') {
                $files[] = $file->getPathname();
            }
        }
        
        return $files;
    }
    
    /**
 * Render string translation page
 */
    public function render_strings_page() {
        $languages = LF_Core::instance()->get_active_languages();
        $default_lang = LF_Core::instance()->get_default_language();

        // Auto-scan theme for strings on page load
        $this->scan_theme_for_strings();

        // Handle save
        if (isset($_POST['lf_save_strings']) && wp_verify_nonce(isset($_POST['lf_strings_nonce']) ? sanitize_text_field(wp_unslash($_POST['lf_strings_nonce'])) : '', 'lf_save_strings')) {
            $this->save_string_translations();
            echo '<div class="notice notice-success"><p>' . esc_html__('Translations saved.', 'langforge') . '</p></div>';
        }

        // Handle delete domain
        if (isset($_GET['delete_domain']) && isset($_GET['_wpnonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'lf_delete_domain')) {
            $domain_to_delete = sanitize_text_field(wp_unslash($_GET['delete_domain']));
            LF_String_Translation::instance()->delete_domain($domain_to_delete);
            // Redirect to clean URL
            wp_redirect(admin_url('admin.php?page=lf-strings&deleted=1')); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- internal admin_url target, not user-controlled
            exit;
        }

        // Show deleted message after redirect
        if (isset($_GET['deleted'])) {
            echo '<div class="notice notice-success"><p>' . esc_html__('Domain deleted.', 'langforge') . '</p></div>';
        }

        // Handle add new string — BEFORE loading domains so new domain appears immediately
        if (isset($_POST['lf_add_string']) && wp_verify_nonce(isset($_POST['lf_add_string_nonce']) ? sanitize_text_field(wp_unslash($_POST['lf_add_string_nonce'])) : '', 'lf_add_string')) {
            $name = sanitize_text_field(wp_unslash($_POST['string_name'] ?? ''));
            $value = sanitize_textarea_field(wp_unslash($_POST['string_value'] ?? ''));
            $domain = sanitize_text_field(wp_unslash($_POST['string_domain'] ?? ''));

            if ($name && $value) {
                LF_String_Translation::instance()->register_string($value, $name, $domain);
                echo '<div class="notice notice-success"><p>' . esc_html__('String added.', 'langforge') . '</p></div>';
            }
        }

        // Load domains AFTER add/delete so the list is always up-to-date
        $domains = LF_String_Translation::instance()->get_domains();

        $current_domain = isset($_GET['domain']) ? sanitize_text_field(wp_unslash($_GET['domain'])) : '';
        $current_lang = isset($_GET['lang']) ? sanitize_text_field(wp_unslash($_GET['lang'])) : '';

        $strings = LF_String_Translation::instance()->get_strings($current_domain ?: null, $default_lang);
        $domain_counts = $this->get_domain_counts();
        ?>
        <div class="wrap">
            <?php LF_Admin_Tabs::render('lf-strings'); ?>
            <?php LF_License::instance()->render_upgrade_banner(); ?>
            <h1><?php esc_html_e('String Translation', 'langforge'); ?><?php Forge_Docs_Link::render('langforge', 'translating-theme-text-string-translation', __('String translation', 'langforge'), __('Translate theme & plugin text strings scanned from your code.', 'langforge')); ?></h1>
            
            <div class="lf-string-filters">
                <form method="get" action="">
                    <input type="hidden" name="page" value="lf-strings">
                    
                    <label><?php esc_html_e('Domain:', 'langforge'); ?></label>
                    <select name="domain">
                        <option value="" <?php selected($current_domain, ''); ?>><?php esc_html_e('All domains', 'langforge'); ?></option>
                        <?php foreach ($domains as $domain):
                            $count = isset($domain_counts[$domain]) ? $domain_counts[$domain] : 0;
                        ?>
                            <option value="<?php echo esc_attr($domain); ?>" <?php selected($current_domain, $domain); ?>>
                                <?php echo esc_html($domain); ?> (<?php echo esc_html($count); ?>)
                            </option>
                        <?php endforeach; ?>
                    </select>
                    
                    <label><?php esc_html_e('Translate to:', 'langforge'); ?></label>
                    <select name="lang">
                        <option value=""><?php esc_html_e('All languages', 'langforge'); ?></option>
                        <?php foreach ($languages as $code => $lang): ?>
                            <?php if ($code === $default_lang) continue; ?>
                            <option value="<?php echo esc_attr($code); ?>" <?php selected($current_lang, $code); ?>>
                                <?php echo esc_html($lang['native_name']); ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                    
                    <input type="submit" class="button lf-btn-apply" value="<?php esc_attr_e('Apply', 'langforge'); ?>">

                    <?php if ($current_domain && $current_domain !== 'default' && in_array($current_domain, $domains)): ?>
                        <a href="<?php echo esc_url(wp_nonce_url(admin_url('admin.php?page=lf-strings&delete_domain=' . $current_domain), 'lf_delete_domain')); ?>"
                           class="button lf-btn-danger"
                           onclick="return confirm('<?php esc_attr_e('Delete all strings in this domain?', 'langforge'); ?>');">
                            <?php esc_html_e('Delete Domain', 'langforge'); ?>
                        </a>
                    <?php endif; ?>
                </form>
            </div>
            
            <h2><?php esc_html_e('Translate Strings', 'langforge'); ?></h2>
            
            <?php
            // Get all unique strings (group by name)
            $unique_strings = $this->get_unique_strings($current_domain);

            // pagination — strings can run into the thousands once
            // theme/plugin domains get auto-registered, and pasting that as one
            // table is hostile UX. We paginate in PHP after dedup so source-id
            // grouping still works.
            $allowed_per_page = [25, 50, 100, 200];
            $per_page = isset($_GET['per_page']) ? (int) $_GET['per_page'] : 25;
            if (!in_array($per_page, $allowed_per_page, true)) $per_page = 25;
            $total_strings = count($unique_strings);
            $total_pages   = max(1, (int) ceil($total_strings / $per_page));
            $current_page  = isset($_GET['paged']) ? max(1, (int) $_GET['paged']) : 1;
            if ($current_page > $total_pages) $current_page = $total_pages;
            $paged_strings = array_slice($unique_strings, ($current_page - 1) * $per_page, $per_page, true);
            $has_filter    = (bool) ($current_domain || $current_lang);
            $clear_url     = admin_url('admin.php?page=lf-strings');
            ?>

            <?php if (empty($unique_strings)): ?>
                <?php if ($has_filter): ?>
                    <div class="lf-empty-state" style="background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:18px 22px;margin:12px 0;color:#92400e;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
                        <span class="dashicons dashicons-filter" style="font-size:20px;width:20px;height:20px;"></span>
                        <div style="flex:1;min-width:240px;">
                            <strong><?php esc_html_e('No strings match your filter.', 'langforge'); ?></strong><br>
                            <span style="font-size:14px;"><?php
                                $bits = [];
                                /* translators: %s: the domain name being filtered on. */
                                if ($current_domain) $bits[] = sprintf(__('domain "%s"', 'langforge'), esc_html($current_domain));
                                /* translators: %s: the language code being filtered on. */
                                if ($current_lang)   $bits[] = sprintf(__('language "%s"', 'langforge'), esc_html(strtoupper($current_lang)));
                                /* translators: %s: a comma-separated list of the active filters. */
                                printf(esc_html__('Active filter: %s.', 'langforge'), implode(', ', $bits)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- each $bits element already esc_html'd above
                            ?></span>
                        </div>
                        <a href="<?php echo esc_url($clear_url); ?>" class="button"><?php esc_html_e('Clear filter', 'langforge'); ?></a>
                    </div>
                <?php else: ?>
                    <p><?php esc_html_e('No strings found. Add strings using the form above or register them in your theme/plugin code.', 'langforge'); ?></p>
                <?php endif; ?>
            <?php else:
                // Limit the rendered list to the current page; keep $unique_strings intact for AI bulk count if needed.
                $unique_strings = $paged_strings;
            ?>
                <?php // results summary + pagination top + per-page selector ?>
                <div class="lf-strings-toolbar" style="display:flex;justify-content:space-between;align-items:center;gap:12px;margin:14px 0 8px;flex-wrap:wrap;">
                    <div style="font-size:14px;color:#475569;">
                        <?php
                        $from_idx = ($current_page - 1) * $per_page + 1;
                        $to_idx   = min($from_idx + count($unique_strings) - 1, $total_strings);
                        printf(
                            /* translators: 1: first row number shown, 2: last row number shown, 3: total number of strings. */
                            esc_html__('Showing %1$d – %2$d of %3$d strings', 'langforge'),
                            esc_html($from_idx), esc_html($to_idx), esc_html($total_strings)
                        );
                        if ($has_filter) {
                            echo ' · <a href="' . esc_url($clear_url) . '" style="color:#dc2626;text-decoration:none;">' . esc_html__('Clear filter', 'langforge') . '</a>';
                        }
                        ?>
                    </div>
                    <?php
                    // Build pagination + per-page-aware base URL
                    $base = remove_query_arg(['paged']);
                    ?>
                    <form method="get" action="" style="display:inline-flex;align-items:center;gap:6px;font-size:14px;color:#64748b;">
                        <input type="hidden" name="page" value="lf-strings">
                        <?php if ($current_domain): ?><input type="hidden" name="domain" value="<?php echo esc_attr($current_domain); ?>"><?php endif; ?>
                        <?php if ($current_lang): ?><input type="hidden" name="lang" value="<?php echo esc_attr($current_lang); ?>"><?php endif; ?>
                        <label><?php esc_html_e('Per page:', 'langforge'); ?></label>
                        <select name="per_page" onchange="this.form.submit()">
                            <?php foreach ($allowed_per_page as $opt): ?>
                                <option value="<?php echo esc_attr($opt); ?>" <?php selected($per_page, $opt); ?>><?php echo esc_html($opt); ?></option>
                            <?php endforeach; ?>
                        </select>
                    </form>
                </div>

                <?php
                // Render compact pagination once (will repeat below the table)
                $render_pager = function() use ($current_page, $total_pages, $current_domain, $current_lang, $per_page) {
                    if ($total_pages <= 1) return;
                    $build = function($p) use ($current_domain, $current_lang, $per_page) {
                        $args = ['page' => 'lf-strings', 'paged' => $p, 'per_page' => $per_page];
                        if ($current_domain) $args['domain'] = $current_domain;
                        if ($current_lang)   $args['lang']   = $current_lang;
                        return esc_url(add_query_arg($args, admin_url('admin.php')));
                    };
                    echo '<nav class="lf-pagination" style="display:flex;gap:4px;justify-content:center;margin:12px 0;flex-wrap:wrap;">';
                    if ($current_page > 1) {
                        echo '<a class="button button-small" href="' . $build($current_page - 1) . '">&lsaquo; ' . esc_html__('Previous', 'langforge') . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $build returns esc_url'd href
                    }
                    $shown = [];
                    $add = function($p) use (&$shown, $current_page, $build, $total_pages) {
                        if ($p < 1 || $p > $total_pages || isset($shown[$p])) return;
                        $shown[$p] = true;
                        $cls = 'button button-small' . ($p === $current_page ? ' button-primary' : '');
                        echo '<a class="' . esc_attr($cls) . '" href="' . $build($p) . '">' . esc_html($p) . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $build returns esc_url'd href
                    };
                    $add(1);
                    if ($current_page > 4) echo '<span style="padding:4px 6px;color:#94a3b8;">&hellip;</span>';
                    for ($p = $current_page - 2; $p <= $current_page + 2; $p++) $add($p);
                    if ($current_page < $total_pages - 3) echo '<span style="padding:4px 6px;color:#94a3b8;">&hellip;</span>';
                    $add($total_pages);
                    if ($current_page < $total_pages) {
                        echo '<a class="button button-small" href="' . $build($current_page + 1) . '">' . esc_html__('Next', 'langforge') . ' &rsaquo;</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $build returns esc_url'd href
                    }
                    echo '</nav>';
                };
                $render_pager();
                ?>
                <form method="post" action="">
                    <?php wp_nonce_field('lf_save_strings', 'lf_strings_nonce'); ?>
                    <input type="hidden" name="lf_domain" value="<?php echo esc_attr($current_domain); ?>">
                    <?php // Per-string domain mapping for "All domains" mode ?>
                    <?php foreach ($unique_strings as $sn => $sd): ?>
                        <input type="hidden" name="string_domains[<?php echo esc_attr($sn); ?>]" value="<?php echo esc_attr($sd['domain']); ?>">
                    <?php endforeach; ?>
                    
                    <?php // (2026-04-30): wrap the matrix in a horizontally
                          // scrollable container so 8+ active languages don't
                          // squeeze each column down to 1-2 chars wide.
                          // The first three columns (Name / Domain / Source-lang)
                          // become position:sticky inside the wrap so the
                          // translator always sees what string they're editing
                          // even after scrolling far right. ?>
                    <div class="lf-strings-tablewrap">
                    <table class="widefat lf-strings-table">
                        <thead>
                            <tr>
                                <th class="lf-col-name lf-col-sticky"><?php esc_html_e('Name', 'langforge'); ?></th>
                                <?php if (!$current_domain): ?>
                                    <th class="lf-col-domain lf-col-sticky"><?php esc_html_e('Domain', 'langforge'); ?></th>
                                <?php endif; ?>
                                <th class="lf-col-lang lf-col-sticky"><?php esc_html_e('Language', 'langforge'); ?></th>
                                <?php // (2026-04-30): when filter "Translate
                                      // to: <lang>" narrows the matrix to a single
                                      // target column, the source value column was
                                      // omitted — translator only saw the technical
                                      // string_name, not the human source text. Add
                                      // an explicit Source column in this mode. ?>
                                <?php if ($current_lang): ?>
                                    <th class="lf-col-source"><?php esc_html_e('Source', 'langforge'); ?></th>
                                <?php endif; ?>
                                <?php foreach ($languages as $code => $lang): ?>
                                    <?php if ($current_lang && $current_lang !== $code) continue; ?>
                                    <th><?php echo esc_html($lang['native_name']); ?></th>
                                <?php endforeach; ?>
                            </tr>
                        </thead>
                        <tbody>
                            <?php foreach ($unique_strings as $string_name => $string_data): ?>
                            <tr>
                                <td class="lf-col-name lf-col-sticky">
                                    <code><?php echo esc_html($string_name); ?></code>
                                    <button type="button" class="lf-copy-name" data-name="<?php echo esc_attr($string_name); ?>" title="<?php esc_attr_e('Copy name', 'langforge'); ?>">
                                        <span class="dashicons dashicons-clipboard"></span>
                                    </button>
                                </td>
                                <?php if (!$current_domain): ?>
                                    <td class="lf-col-domain lf-col-sticky"><span class="lf-domain-badge"><?php echo esc_html($string_data['domain']); ?></span></td>
                                <?php endif; ?>
                                <td class="lf-col-lang lf-col-sticky"><strong><?php echo esc_html(strtoupper($string_data['source_lang'])); ?></strong></td>
                                <?php if ($current_lang): ?>
                                    <?php
                                    // source_value = whatever's stored under
                                    // the source language for this row. Read-only
                                    // textarea so the translator can copy text from
                                    // it but accidentally typing here doesn't write
                                    // back to the source.
                                    $source_value = isset($string_data['translations'][$string_data['source_lang']])
                                        ? $string_data['translations'][$string_data['source_lang']]
                                        : '';
                                    ?>
                                    <td class="lf-col-source">
                                        <textarea readonly rows="2" class="widefat lf-source-readonly"
                                                  aria-label="<?php esc_attr_e('Source value (read-only)', 'langforge'); ?>"><?php echo esc_textarea($source_value); ?></textarea>
                                    </td>
                                <?php endif; ?>
                                <?php foreach ($languages as $code => $lang): ?>
                                    <?php if ($current_lang && $current_lang !== $code) continue; ?>
                                    <?php
                                    $trans_value = isset($string_data['translations'][$code]) ? $string_data['translations'][$code] : '';
                                    $is_source = ($code === $string_data['source_lang']);
                                    ?>
                                    <td>
                                        <textarea name="strings[<?php echo esc_attr($string_name); ?>][<?php echo esc_attr($code); ?>]"
                                                  class="widefat" rows="2" <?php echo $is_source ? 'style="background:#f0f0f0;"' : ''; ?>><?php echo esc_textarea($trans_value); ?></textarea>
                                        <?php if ($is_source): ?>
                                            <small style="color:#666;"><?php esc_html_e('(source)', 'langforge'); ?></small>
                                        <?php endif; ?>
                                    </td>
                                <?php endforeach; ?>
                            </tr>
                            <?php endforeach; ?>
                        </tbody>
                    </table>
                    </div><!-- /.lf-strings-tablewrap -->
                    
                    <?php $render_pager(); ?>

                    <?php
                    $ai_enabled      = class_exists('LF_License') && LF_License::can('ai_translation');
                    $ai_configured   = $ai_enabled && class_exists('LF_AI_Client') && LF_AI_Client::instance()->is_configured();

                    // True cost of the whole-domain AI run.
                    // The shared credit badge only shows the per-translation UNIT
                    // cost (translate = 1), which reads as "this whole run costs 1
                    // credit". In reality ajax_ai_translate_strings_bulk translates
                    // every still-empty (string x target-language) slot at 1 credit
                    // each, so a domain with many strings/languages silently spends
                    // far more than the "1" shown on the button. Pre-compute the real
                    // total here with the SAME loop the handler uses, and surface it
                    // on the button + in the confirm dialog so the cost is never a
                    // surprise. Recomputed on every render — and since changing the
                    // domain reloads the page, the figure tracks the current domain.
                    $ai_unit_cost    = class_exists('Forge_Credits_Widget') ? (int) Forge_Credits_Widget::get_action_cost('translate') : 1;
                    if ($ai_unit_cost < 1) { $ai_unit_cost = 1; }
                    // Two modes (LF UX): "Translate Missing" fills only empty slots;
                    // "Re-Translate All" regenerates EVERY translatable slot (string ×
                    // target-language with a non-empty source), overwriting existing
                    // values. Count both here so each button shows its own true cost.
                    // A slot counts as "missing" only when the target is empty — and a
                    // value like "\", "1" or a leftover Translation-Memory string DOES
                    // count as present, which is exactly why "Translate Missing" can be
                    // 0 while "Re-Translate All" still has work.
                    $ai_pending       = 0;  // empty target slots → "Translate Missing"
                    $ai_all_slots     = 0;  // every translatable slot → "Re-Translate All"
                    $ai_missing_langs = [];
                    $ai_all_langs     = [];
                    if ($ai_enabled && $current_domain) {
                        // Re-fetch the FULL domain set: $unique_strings is reduced to
                        // the current page by the pagination slice above, but the bulk
                        // handler runs over EVERY string in the domain.
                        $ai_all_strings = $this->get_unique_strings($current_domain);
                        foreach ($languages as $ai_tlang => $ai_tinfo) {
                            if ($ai_tlang === $default_lang) { continue; }
                            foreach ($ai_all_strings as $ai_sdata) {
                                $ai_src = isset($ai_sdata['translations'][$ai_sdata['source_lang']]) ? $ai_sdata['translations'][$ai_sdata['source_lang']] : '';
                                if ($ai_src === '') { continue; } // no source text → not translatable in either mode
                                $ai_all_slots++;
                                $ai_all_langs[$ai_tlang] = true;
                                if (empty($ai_sdata['translations'][$ai_tlang])) {
                                    $ai_pending++;
                                    $ai_missing_langs[$ai_tlang] = true;
                                }
                            }
                        }
                    }
                    $ai_missing_cost       = $ai_pending   * $ai_unit_cost;
                    $ai_all_cost           = $ai_all_slots * $ai_unit_cost;
                    $ai_missing_lang_count = count($ai_missing_langs);
                    $ai_all_lang_count     = count($ai_all_langs);

                    // Cost chip ("crystal + N credits"). NOT a *-credits-badge: that
                    // class is for per-action unit badges auto-refreshed via
                    // render_badge_html; this is a computed run total, so it carries
                    // no data-forge-cost-action (the shared widget JS must leave it
                    // alone). Translucent-white pill works on both button colours.
                    $ai_cost_chip = function ($cost) {
                        $label = sprintf(
                            /* translators: %s = number of credits */
                            _n('%s credit', '%s credits', $cost, 'langforge'),
                            number_format_i18n($cost)
                        );
                        return ' <span class="lf-ai-run-cost" title="'
                            /* translators: %s: the estimated credit cost label. */
                            . esc_attr(sprintf(__('Estimated cost: %s', 'langforge'), $label))
                            . '" style="display:inline-flex;align-items:center;gap:4px;background:rgba(255,255,255,.18);border-radius:10px;padding:1px 9px;font-size:12px;font-weight:700;line-height:1.7;vertical-align:middle;">'
                            . '<svg viewBox="0 0 24 22" width="12" height="11" fill="currentColor" aria-hidden="true"><polygon points="7,4 17,4 21,9 12,19 3,9"/></svg>'
                            . esc_html($label) . '</span>';
                    };
                    $ai_pro_badge = ' <span style="background:#fff4e5;color:#b45309;border:1px solid #fcd9a8;border-radius:3px;font-size:10px;font-weight:700;padding:1px 5px;letter-spacing:.4px;text-transform:uppercase;line-height:1;">PRO</span>';

                    // Single dynamic button. Its chip shows the MISSING cost (0
                    // when everything is already translated); clicking opens a
                    // modal that explains the state and offers Re-Translate / Cancel
                    // with the regenerate cost.
                    $ai_btn_inner = '<span class="dashicons dashicons-translation" style="font-size:14px;width:14px;height:14px;vertical-align:text-bottom;margin-right:2px;"></span> ' . esc_html__('AI Translate', 'langforge');
                    if (!$ai_enabled) {
                        $ai_btn_inner .= $ai_pro_badge;
                    } elseif ($current_domain) {
                        $ai_btn_inner .= $ai_cost_chip($ai_missing_cost);
                    } elseif (class_exists('Forge_Credits_Widget')) {
                        // No domain selected -> bulk can't run yet; show the unit badge.
                        $ai_btn_inner .= ' ' . Forge_Credits_Widget::render_badge_html('translate', 'lf');
                    }
                    $ai_btn_style = 'background:linear-gradient(135deg,#0EC49E,#0A9B7C)!important;color:#fff!important;border:none!important;font-weight:600!important;';
                    if (!$ai_configured) {
                        $ai_btn_style .= 'opacity:.55!important;cursor:not-allowed!important;';
                    }
                    ?>
                    <p class="submit lf-strings-actions" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
                        <input type="submit" name="lf_save_strings" class="button button-primary" value="<?php esc_attr_e('Save Translations', 'langforge'); ?>">
                        <button type="button" id="lf-ai-translate" class="button" style="<?php echo esc_attr($ai_btn_style); ?>"
                                data-domain="<?php echo esc_attr($current_domain); ?>"
                                data-source="<?php echo esc_attr($default_lang); ?>"
                                data-missing-cost="<?php echo (int) $ai_missing_cost; ?>"
                                data-missing-slots="<?php echo (int) $ai_pending; ?>"
                                data-missing-langs="<?php echo (int) $ai_missing_lang_count; ?>"
                                data-all-cost="<?php echo (int) $ai_all_cost; ?>"
                                data-all-slots="<?php echo (int) $ai_all_slots; ?>"
                                data-all-langs="<?php echo (int) $ai_all_lang_count; ?>"
                                data-ai-enabled="<?php echo $ai_enabled ? '1' : '0'; ?>"
                                data-ai-configured="<?php echo $ai_configured ? '1' : '0'; ?>"
                                <?php disabled(!$ai_configured); ?>>
                            <?php echo $ai_btn_inner; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built button markup ?>
                        </button>
                        <?php if (!$current_domain): ?>
                            <span style="font-size:14px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:4px 10px;">
                                <span class="dashicons dashicons-info" style="font-size:14px;width:12px;height:12px;vertical-align:text-bottom;"></span>
                                <?php esc_html_e('Pick a domain in the filter to enable bulk AI translate.', 'langforge'); ?>
                            </span>
                        <?php elseif (!$ai_enabled): ?>
                            <span style="font-size:14px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:4px 10px;">
                                <span class="dashicons dashicons-lock" style="font-size:14px;width:12px;height:12px;vertical-align:text-bottom;"></span>
                                <?php esc_html_e('Activate Lang Forge Pro to enable AI translation for this domain.', 'langforge'); ?>
                            </span>
                        <?php elseif (!$ai_configured): ?>
                            <span style="font-size:14px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:4px 10px;">
                                <span class="dashicons dashicons-lock" style="font-size:14px;width:12px;height:12px;vertical-align:text-bottom;"></span>
                                <?php esc_html_e('Connect to Avakode to enable AI translation for this domain.', 'langforge'); ?>
                            </span>
                        <?php endif; ?>
                        <span id="lf-strings-dirty" style="display:none;font-size:14px;color:#dc2626;font-weight:600;margin-left:8px;">
                            <span class="dashicons dashicons-warning" style="font-size:14px;width:14px;height:14px;vertical-align:text-bottom;"></span>
                            <?php esc_html_e('You have unsaved changes.', 'langforge'); ?>
                        </span>
                    </p>

                    <!-- AI Translate confirmation modal (populated by JS) -->
                    <div id="lf-ai-modal" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="lf-ai-modal-title"
                         style="display:none;position:fixed;inset:0;z-index:100000;background:rgba(15,23,42,.5);align-items:center;justify-content:center;padding:20px;">
                        <div style="background:#fff;border-radius:12px;max-width:480px;width:100%;padding:24px;box-shadow:0 12px 48px rgba(15,23,42,.25);">
                            <h2 id="lf-ai-modal-title" style="margin:0 0 12px;font-size:18px;font-weight:700;color:#1e293b;"></h2>
                            <p id="lf-ai-modal-body" style="margin:0 0 22px;color:#475569;font-size:14px;line-height:1.55;"></p>
                            <div id="lf-ai-modal-actions" style="display:flex;justify-content:flex-end;flex-wrap:wrap;align-items:center;"></div>
                        </div>
                    </div>

                    <script>
 // warn before navigating away with unsaved changes
                    (function(){
                        var dirty = false;
                        jQuery('.lf-strings-table textarea').on('input', function(){
                            if (!dirty) { dirty = true; jQuery('#lf-strings-dirty').show(); }
                        });
                        jQuery('form').on('submit', function(){ dirty = false; });
                        window.addEventListener('beforeunload', function(e){
                            if (!dirty) return;
                            e.preventDefault();
                            e.returnValue = '';
                        });
                    })();
                    jQuery('.lf-copy-name').on('click', function(e){
                        e.preventDefault();
                        var name = jQuery(this).data('name');
                        navigator.clipboard.writeText(name).then(function(){
                            var $icon = jQuery(e.currentTarget).find('.dashicons');
                            $icon.removeClass('dashicons-clipboard').addClass('dashicons-yes');
                            setTimeout(function(){ $icon.removeClass('dashicons-yes').addClass('dashicons-clipboard'); }, 1000);
                        });
                    });
 // bulk AI translate now runs on the backend over the
                    // ENTIRE domain (not just the visible paginated page) and
                    // saves each translation directly to the DB. After the call
                    // we reload, so pagination and any already-typed edits on
                    // the current page both survive.
                    (function(){
                        var $modal = jQuery('#lf-ai-modal');
                        function closeModal(){ $modal.css('display','none').attr('aria-hidden','true'); }
                        function crWord(n){ return n === 1 ? '<?php echo esc_js(__('credit', 'langforge')); ?>' : '<?php echo esc_js(__('credits', 'langforge')); ?>'; }

                        function runBulk($btn, mode){
                            var origInner = $btn.data('orig-inner') || $btn.html();
                            $btn.data('orig-inner', origInner);
                            $btn.prop('disabled',true).css('opacity',0.6).html('<span class="lf-spinner" style="width:14px;height:14px;margin:0 6px 0 0;vertical-align:text-bottom;border-color:rgba(255,255,255,.45);border-top-color:#fff;"></span> <?php echo esc_js(__('Translating the whole domain… this can take a minute', 'langforge')); ?>');
                            var restore = function(){
                                $btn.prop('disabled', <?php echo $ai_configured ? 'false' : 'true'; ?>)
                                    .css('opacity', <?php echo $ai_configured ? '1' : '0.55'; ?>).html(origInner);
                            };
                            jQuery.post(lfAdmin.ajaxUrl, {
                                action: 'lf_ai_translate_strings_bulk',
                                nonce: lfAdmin.nonce,
                                domain: $btn.data('domain'),
                                mode: mode
                            }, function(r){
                                if (r && r.success) {
                                    var n = (r.data && r.data.translated) || 0;
                                    var errs = (r.data && r.data.errors) || [];
                                    var msg2 = '<?php /* translators: %d: number of strings translated (replaced client-side). */ echo esc_js(__('Translated %d strings. Reloading…', 'langforge')); ?>'.replace('%d', n);
                                    if (errs.length) msg2 += '\n\n' + errs.join('\n');
                                    $btn.html('<span class="dashicons dashicons-yes"></span> ' + msg2);
                                    setTimeout(function(){ location.reload(); }, 600);
                                } else {
                                    alert((r && r.data && r.data.message) ? r.data.message : 'Error');
                                    restore();
                                }
                            }).fail(function(){
                                alert('<?php echo esc_js(__('Network error. Please try again.', 'langforge')); ?>');
                                restore();
                            });
                        }

                        jQuery('#lf-ai-translate').on('click', function(){
                            var $btn = jQuery(this);
                            if (!$btn.data('domain')) { alert('<?php echo esc_js(__('Please select a specific domain first.', 'langforge')); ?>'); return; }
                            if (String($btn.data('ai-enabled')) !== '1') { alert('<?php echo esc_js(__('AI translation requires an active Lang Forge Pro license.', 'langforge')); ?>'); return; }
                            if (String($btn.data('ai-configured')) !== '1') { alert('<?php echo esc_js(__('Connect to Avakode before using AI translation.', 'langforge')); ?>'); return; }
                            // Offer to save unsaved edits before running (reload follows).
                            if (jQuery('#lf-strings-dirty').is(':visible')) {
                                if (!confirm('<?php echo esc_js(__('You have unsaved edits on this page. They will be lost when the page reloads after AI translation. Save them first?', 'langforge')); ?>')) return;
                                jQuery('form input[name="lf_save_strings"]').closest('form').trigger('submit');
                                return;
                            }

                            var missing = parseInt($btn.data('missing-slots'),10)||0;
                            var missingCost = parseInt($btn.data('missing-cost'),10)||0;
                            var missingLangs = parseInt($btn.data('missing-langs'),10)||0;
                            var allSlots = parseInt($btn.data('all-slots'),10)||0;
                            var allCost = parseInt($btn.data('all-cost'),10)||0;

                            var title, body, actions = [];
                            if (missing > 0) {
                                title = '<?php echo esc_js(__('Translate missing strings?', 'langforge')); ?>';
                                body  = '<?php /* translators: 1: number of untranslated strings, 2: number of languages, 3: credit cost, 4: the word "credit"/"credits". */ echo esc_js(__('%1$s string(s) across %2$s language(s) have no translation yet. Translating them costs %3$s %4$s, and the result is saved automatically.', 'langforge')); ?>'
                                        .replace('%1$s', missing).replace('%2$s', missingLangs).replace('%3$s', missingCost).replace('%4$s', crWord(missingCost));
                                actions.push({ label: '<?php echo esc_js(__('Translate Missing', 'langforge')); ?> · ' + missingCost + ' ' + crWord(missingCost), mode:'missing', color:'green' });
                                if (allSlots > missing) {
                                    actions.push({ label: '<?php echo esc_js(__('Re-Translate All', 'langforge')); ?> · ' + allCost + ' ' + crWord(allCost), mode:'all', color:'amber' });
                                }
                            } else if (allSlots > 0) {
                                title = '<?php echo esc_js(__('Everything is already translated', 'langforge')); ?>';
                                body  = '<?php /* translators: 1: credit cost to re-translate all, 2: the word "credit"/"credits". */ echo esc_js(__('All strings in this domain already have a translation, so there is nothing new to translate — that is why the cost shows 0. You can re-generate every translation from scratch with the latest glossary: this overwrites the existing values, ignores Translation Memory, and costs %1$s %2$s.', 'langforge')); ?>'
                                        .replace('%1$s', allCost).replace('%2$s', crWord(allCost));
                                actions.push({ label: '<?php echo esc_js(__('Re-Translate All', 'langforge')); ?> · ' + allCost + ' ' + crWord(allCost), mode:'all', color:'amber' });
                            } else {
                                title = '<?php echo esc_js(__('Nothing to translate', 'langforge')); ?>';
                                body  = '<?php echo esc_js(__('There are no translatable strings in this domain yet — add some source text first.', 'langforge')); ?>';
                            }

                            jQuery('#lf-ai-modal-title').text(title);
                            jQuery('#lf-ai-modal-body').text(body);
                            var $acts = jQuery('#lf-ai-modal-actions').empty();
                            actions.forEach(function(a){
                                var bg = (a.color === 'amber')
                                    ? 'background:linear-gradient(135deg,#f59e0b,#d97706)!important;'
                                    : 'background:linear-gradient(135deg,#0EC49E,#0A9B7C)!important;';
                                jQuery('<button type="button" class="button">')
                                    .attr('style', bg + 'color:#fff!important;border:none!important;font-weight:600!important;margin-left:8px;')
                                    .text(a.label)
                                    .on('click', function(){ closeModal(); runBulk($btn, a.mode); })
                                    .appendTo($acts);
                            });
                            jQuery('<button type="button" class="button">')
                                .attr('style', 'margin-left:8px;')
                                .text('<?php echo esc_js(__('Cancel', 'langforge')); ?>')
                                .on('click', closeModal)
                                .appendTo($acts);

                            $modal.css('display','flex').attr('aria-hidden','false');
                        });

                        // Dismiss on backdrop click or Esc.
                        $modal.on('click', function(e){ if (e.target === this) closeModal(); });
                        jQuery(document).on('keydown', function(e){ if (e.key === 'Escape' && $modal.is(':visible')) closeModal(); });
                    })();
                    </script>
                </form>
            <?php endif; ?>
            
            <hr>
            
            <h2><?php esc_html_e('Add New String', 'langforge'); ?></h2>
            <form method="post" action="" class="lf-add-string-form">
                <?php wp_nonce_field('lf_add_string', 'lf_add_string_nonce'); ?>
                <table class="form-table">
                    <tr>
                        <th><label for="string_name"><?php esc_html_e('String Name (ID)', 'langforge'); ?></label></th>
                        <td><input type="text" name="string_name" id="string_name" class="regular-text" required></td>
                    </tr>
                    <tr>
                        <th><label for="string_value"><?php esc_html_e('Default Value', 'langforge'); ?></label></th>
                        <td><textarea name="string_value" id="string_value" class="large-text" rows="2" required></textarea></td>
                    </tr>
                    <tr>
                        <th><label for="string_domain"><?php esc_html_e('Domain', 'langforge'); ?></label></th>
                        <td><input type="text" name="string_domain" id="string_domain" class="regular-text" value="default"></td>
                    </tr>
                </table>
                <p>
                    <input type="submit" name="lf_add_string" class="button button-secondary" value="<?php esc_attr_e('Add String', 'langforge'); ?>">
                </p>
            </form>
            
            <hr>
            
            <h2><?php esc_html_e('Usage in Theme', 'langforge'); ?></h2>
            <p><?php _e('Strings from your theme are registered automatically via standard WordPress translation functions. Just use <code>__()</code> and <code>_e()</code> — Lang Forge picks them up.', 'langforge'); // phpcs:ignore WordPress.Security.EscapeOutput.UnsafePrintingFunction -- string literal contains intentional HTML markup ?></p>
            <pre style="background:#f0f0f0;padding:15px;overflow:auto;border-radius:8px;font-size:14px;">
// Standard WordPress functions — strings auto-register
echo __('Welcome to our site', 'your-theme');
esc_html_e('Contact us', 'your-theme');
echo esc_html__('Read more', 'your-theme');

// Shortcode in content (for editors)
[lf_string name="welcome_message" domain="your-theme"]Welcome to our site[/lf_string]
            </pre>
            <p style="font-size:14px;color:#64748b;"><?php esc_html_e('Configure which text domains to auto-register in Settings → String Auto-Registration Domains.', 'langforge'); ?></p>
        </div>
        
        <style>
            .lf-string-filters { margin: 20px 0; padding: 15px; background: #f5f5f5; }
            .lf-string-filters form { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
            .lf-string-filters label { margin-right: 5px; }
            .lf-string-filters select { margin-right: 15px; }

            /* (2026-04-30): horizontal-scroll wrap so 8+ language
 columns don't crush the matrix. The wrapper takes any overflow
 beyond the page width; the table inside grows naturally as
 language columns are added.

 table-layout stays auto (was fixed) so language columns size
 to their content + min-width below, instead of dividing the
 viewport equally and ending up at 80px each.

 Sticky first columns (.lf-col-sticky) keep Name / Domain /
 Language visible while the user scrolls right through the
 language matrix, so they always know which row they're
 editing. Stacking via left:0 / left:Xpx — calc'd by adding
 up the widths of the sticky columns to its left. */
            /* Clamp the wrap to the actual viewport. WordPress admin's
 .wrap has no explicit width, so a wider-than-viewport child
 (our 12-column matrix) makes the whole admin page scroll
 horizontally — sidebar slides off-screen too. Calc-based
 viewport-relative max-width keeps the scroll INSIDE this
 wrap. 200px = collapsed-sidebar (36px) + expanded-sidebar
 headroom (~120px) + .wrap margins (40px); over-conservative
 on collapsed-sidebar layouts but always fits. */
            .lf-strings-tablewrap { overflow-x: auto; max-width: calc(100vw - 200px); }
            @media (max-width: 782px) { /* WP admin "responsive" breakpoint */
                .lf-strings-tablewrap { max-width: 100vw; }
            }
            /* `widefat` (WP core) sets border-collapse:collapse AND
 overflow:hidden on the table element. Both break
 position:sticky on cells:
 - border-collapse:collapse silently disables sticky on
 cells in Chromium/WebKit;
 - overflow:hidden on the table makes the table itself the
 sticky scroll context, so cells stick to the table edge
 (which doesn't scroll) instead of to the wrap (which
 does).
 Override both: separate borders, visible overflow, and
 re-apply the bottom-border the .widefat zebra normally
 relies on so rows stay visually delineated. */
            .lf-strings-table.widefat { border-collapse: separate; border-spacing: 0; overflow: visible; }
            .lf-strings-table.widefat th, .lf-strings-table.widefat td {
                border-bottom: 1px solid #e5e7eb;
            }
            .lf-strings-table { width: max-content; min-width: 100%; table-layout: auto; }
            /* Force top-align over WP core's higher-specificity `.widefat td`
 rule. Without !important the cells fall back to middle-align, so
 the source column (taller because of the "(source)" caption below
 its textarea) pushes its textarea ~10px above the other columns —
 the inputs end up on different lines. Top-align makes each
 textarea's position independent of the caption. */
            .lf-strings-table th, .lf-strings-table td { vertical-align: top !important; }
            .lf-strings-table textarea { width: 100%; box-sizing: border-box; min-width: 220px; vertical-align: top; }
            /* min-width on the sticky columns is the actual constraint —
 table-layout:auto lets explicit `width` be just a hint when
 cell content is narrower, so the rendered cell shrinks below
 the "left" offset of the next sticky column and the layout
 misaligns. min-width forces ≥ N px so the offsets stay in
 sync with rendered widths. */
            .lf-strings-table .lf-col-name { min-width: 220px; max-width: 320px; word-break: break-all; }
            .lf-strings-table .lf-col-name code { font-size: 14px; }
            .lf-strings-table .lf-col-domain { min-width: 140px; max-width: 180px; white-space: nowrap; }
            .lf-strings-table .lf-col-lang { min-width: 100px; max-width: 120px; white-space: nowrap; }
            .lf-strings-table .lf-col-source { min-width: 240px; max-width: 320px; }
            .lf-strings-table .lf-col-source textarea.lf-source-readonly {
                background: #f1f5f9;
                color: #475569;
                cursor: default;
                border-color: #e2e8f0;
            }
            .lf-strings-table thead th { position: relative; }
            /* Sticky columns: stay fixed during horizontal scroll. The
 z-index is raised so the row hover background and other cells
 render below them; left offsets are stacked manually so
 Domain sits exactly at the right edge of Name, etc. */
            .lf-strings-table .lf-col-sticky { position: sticky; background: #fff; z-index: 2; }
            .lf-strings-table thead .lf-col-sticky { z-index: 3; background: #f6f7f7; }
            .lf-strings-table .lf-col-name.lf-col-sticky    { left: 0; }
            .lf-strings-table .lf-col-domain.lf-col-sticky  { left: 220px; }
            /* When Domain column is hidden (single-domain filter) the
 Language sticky column slides over to where Domain was. The
 PHP doesn't render .lf-col-domain in that mode, so Language's
 left offset is the same as Domain's was — 220px. We pick the
 correct one with a CSS sibling selector. */
            .lf-strings-table .lf-col-name.lf-col-sticky + .lf-col-lang.lf-col-sticky    { left: 220px; }
            .lf-strings-table .lf-col-domain.lf-col-sticky + .lf-col-lang.lf-col-sticky  { left: 360px; }
            /* Subtle right-shadow on the last sticky column so users see
 where the scrolling content begins. */
            .lf-strings-table .lf-col-lang.lf-col-sticky::after {
                content: '';
                position: absolute;
                top: 0; right: 0; bottom: 0;
                width: 6px;
                pointer-events: none;
                background: linear-gradient(to right, rgba(0,0,0,0.06), transparent);
            }
            .lf-admin .wrap .lf-strings-table tbody td { vertical-align: top; }
            .lf-admin .wrap .lf-btn-apply.button { background: #0EC49E !important; border-color: #0EC49E !important; color: #fff !important; border-radius: 8px !important; font-weight: 600 !important; padding: 6px 20px !important; transition: all 0.3s !important; }
            .lf-admin .wrap .lf-btn-apply.button:hover, .lf-admin .wrap .lf-btn-apply.button:focus { background: #0CB38E !important; border-color: #0CB38E !important; color: #fff !important; transform: translateY(-1px); }
            .lf-admin .wrap .lf-btn-danger.button { background: #fff !important; border-color: #EF4444 !important; color: #EF4444 !important; border-radius: 8px !important; font-weight: 600 !important; padding: 6px 20px !important; transition: all 0.3s !important; text-decoration: none !important; }
            .lf-admin .wrap .lf-btn-danger.button:hover, .lf-admin .wrap .lf-btn-danger.button:focus { background: #EF4444 !important; border-color: #EF4444 !important; color: #fff !important; transform: translateY(-1px); }
            .lf-copy-name { background: none !important; border: none !important; padding: 2px 4px !important; cursor: pointer; opacity: 0.4; transition: opacity 0.2s; vertical-align: middle; min-height: 0 !important; }
            .lf-copy-name:hover { opacity: 1; }
            .lf-copy-name .dashicons { font-size: 14px; width: 14px; height: 14px; color: var(--accent, #0EC49E); }
            .lf-domain-badge { background: #f0f0f0; color: #555; padding: 2px 8px; border-radius: 4px; font-size: 14px; }
            .lf-add-string-form { background: #f9f9f9; padding: 15px; margin: 20px 0; }
        </style>
        <?php
    }
    
    /**
 * Save string translations
 */
    private function save_string_translations() {
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified in render_string_translation_page (lf_strings_nonce / lf_save_strings) before this method runs.
        if (empty($_POST['strings']) || !is_array($_POST['strings'])) {
            return;
        }

        $global_domain = isset($_POST['lf_domain']) ? sanitize_text_field(wp_unslash($_POST['lf_domain'])) : '';
        $posted_strings = wp_unslash($_POST['strings']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Keys/values sanitized per-item in the loop below (sanitize_text_field / sanitize_textarea_field).
        $string_domains = (isset($_POST['string_domains']) && is_array($_POST['string_domains']))
            ? wp_unslash($_POST['string_domains']) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values sanitized per-item below (sanitize_text_field).
            : [];

        foreach ($posted_strings as $raw_name => $translations) {
            if (!is_array($translations)) {
                continue;
            }

            $name = sanitize_text_field($raw_name);

            // Use per-string domain if available (for "All domains" mode), else global
            $domain = !empty($string_domains[$raw_name])
                ? sanitize_text_field($string_domains[$raw_name])
                : ($global_domain ?: 'default');

            foreach ($translations as $lang => $value) {
                $lang = sanitize_text_field($lang);
                $value = sanitize_textarea_field($value);

                if (!empty($value)) {
                    LF_String_Translation::instance()->save_translation($name, $domain, $lang, $value);
                }
            }
        }
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }

    /**
 * Add WordPress dashboard widget
 */
    public function add_dashboard_widget() {
        wp_add_dashboard_widget(
            'lf_translation_status',
            __('Translation Status', 'langforge'),
            [$this, 'render_dashboard_widget']
        );
    }

    /**
 * Render dashboard widget
 */
    public function render_dashboard_widget() {
        global $wpdb;

        $languages = LF_Core::instance()->get_active_languages();
        $default_lang = LF_Core::instance()->get_default_language();
        $translatable = LF_Core::get_translatable_post_types();

        if (empty($translatable)) {
            echo '<p>' . esc_html__('No translatable post types configured.', 'langforge') . '</p>';
            return;
        }

        // Count published posts per language across all translatable types
        $type_placeholders = implode(',', array_fill(0, count($translatable), '%s'));
        $query_args = array_merge($translatable, [$default_lang]);

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core tables; IN built from %s placeholders, values are %-prepared.
        $total_default = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Dashboard widget count; computed on demand.
            "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p
            INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
            WHERE p.post_type IN ($type_placeholders) AND p.post_status = 'publish'
            AND pm.meta_key = '_lf_language' AND pm.meta_value = %s",
            ...$query_args
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        if (!$total_default) {
            echo '<p>' . esc_html__('No translatable content found. Start by creating posts and setting their language.', 'langforge') . '</p>';
            return;
        }

        echo '<div class="avl-dashboard-stats">';
        foreach ($languages as $code => $lang) {
            if ($code === $default_lang) continue;

            $args2 = array_merge($translatable, [$code]);
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core tables; IN built from %s placeholders, values are %-prepared.
            $translated = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Dashboard widget count; computed on demand.
                "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                WHERE p.post_type IN ($type_placeholders) AND p.post_status = 'publish'
                AND pm.meta_key = '_lf_language' AND pm.meta_value = %s",
                ...$args2
            ));
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

            $pct = $total_default > 0 ? round(($translated / $total_default) * 100) : 0;
            $color = $pct === 100 ? '#00a32a' : ($pct > 50 ? '#dba617' : '#dc3232');

            printf(
                '<div style="margin-bottom:12px;">
                    <div style="display:flex;justify-content:space-between;margin-bottom:4px;">
                        <strong>%s</strong>
                        <span>%d / %d (%d%%)</span>
                    </div>
                    <div style="height:6px;background:#e0e0e0;border-radius:3px;overflow:hidden;">
                        <div style="height:100%%;width:%d%%;background:%s;border-radius:3px;"></div>
                    </div>
                </div>',
                esc_html($lang['native_name']),
                (int) $translated,
                (int) $total_default,
                esc_html($pct),
                esc_attr($pct),
                esc_attr($color)
            );
        }
        echo '</div>';

        printf(
            '<p><a href="%s" class="button">%s</a></p>',
            esc_url(admin_url('admin.php?page=lf-status')),
            esc_html__('View Full Status', 'langforge')
        );
    }

    /**
 * Render translation status page
 */
    public function render_status_page() {
        global $wpdb;

        $languages = LF_Core::instance()->get_active_languages();
        $default_lang = LF_Core::instance()->get_default_language();
        $translatable = LF_Core::get_translatable_post_types();
        // Use 'filter_type' param — 'post_type' conflicts with WordPress admin routing 
        $filter_type = isset($_GET['filter_type']) ? sanitize_text_field(wp_unslash($_GET['filter_type'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin status filter, no data mutation.
        $filter_lang = isset($_GET['lang']) ? sanitize_text_field(wp_unslash($_GET['lang'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin status filter, no data mutation.
        // LF audit 2026-04-28 (L2): when ?view=outdated, render the
        // out-of-date post list instead of the standard Untranslated table.
        $view = isset($_GET['view']) ? sanitize_text_field(wp_unslash($_GET['view'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin status view selector, no data mutation.
        if (!in_array($view, ['', 'outdated'], true)) {
            $view = '';
        }

        // Validate filter values (prevent wp_die on invalid post types)
        if ($filter_type && !in_array($filter_type, $translatable, true)) {
            $filter_type = '';
        }
        if ($filter_lang && !isset($languages[$filter_lang])) {
            $filter_lang = '';
        }

        // LF audit 2026-04-28 (L2): compute out-of-date counts per
        // target language. A translation post is "out of date" iff it
        // has post-meta `_lf_needs_update` set (LF_Content_Diff writes
        // it whenever the source post's title or content changes after
        // the translation was created). Bulk_actions and roles also
        // touch this key on duplicate-and-translate flows. We count the
        // non-default-language translation posts; the default-language
        // post itself never carries this flag.
        // The count must match the list shown when ?view=outdated, so
        // we restrict to translatable post types and honour the
        // filter_type query arg the same way the Untranslated list
        // does. Without this, an old _lf_needs_update flag on a CPT
        // that was later removed from translatable types would inflate
        // the tile but vanish from the list.
        $outdated_types = $filter_type ? [$filter_type] : $translatable;
        $outdated_type_ph = $outdated_types
            ? implode(',', array_fill(0, count($outdated_types), '%s'))
            : "''";
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core tables; IN built from %s placeholders, values are %-prepared.
        $outdated_total = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $outdated_type_ph is %s placeholders; values %-prepared.
            "SELECT COUNT(DISTINCT pm_flag.post_id)
             FROM {$wpdb->postmeta} pm_flag
             INNER JOIN {$wpdb->posts} p ON p.ID = pm_flag.post_id
                AND p.post_status NOT IN ('trash','auto-draft','inherit')
                AND p.post_type IN ($outdated_type_ph)
             INNER JOIN {$wpdb->postmeta} pm_lang
                ON pm_lang.post_id = pm_flag.post_id
                AND pm_lang.meta_key = '_lf_language'
                AND pm_lang.meta_value <> %s
             WHERE pm_flag.meta_key = '_lf_needs_update'",
            ...array_merge($outdated_types, [$default_lang])
        ));
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        $outdated_by_lang = [];
        foreach ($languages as $code => $lang) {
            if ($code === $default_lang) continue;
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core tables; IN built from %s placeholders, values are %-prepared.
            $outdated_by_lang[$code] = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $outdated_type_ph is %s placeholders; values %-prepared.
                "SELECT COUNT(DISTINCT pm_flag.post_id)
                 FROM {$wpdb->postmeta} pm_flag
                 INNER JOIN {$wpdb->posts} p ON p.ID = pm_flag.post_id
                    AND p.post_status NOT IN ('trash','auto-draft','inherit')
                    AND p.post_type IN ($outdated_type_ph)
                 INNER JOIN {$wpdb->postmeta} pm_lang
                    ON pm_lang.post_id = pm_flag.post_id
                    AND pm_lang.meta_key = '_lf_language'
                    AND pm_lang.meta_value = %s
                 WHERE pm_flag.meta_key = '_lf_needs_update'",
                ...array_merge($outdated_types, [$code])
            ));
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        }
        ?>
        <div class="wrap">
            <?php LF_Admin_Tabs::render('lf-status'); ?>
            <?php LF_License::instance()->render_upgrade_banner(); ?>
            <h1><?php esc_html_e('Translation Status', 'langforge'); ?><?php Forge_Docs_Link::render('langforge', 'translation-analytics-pro', __('Translation status', 'langforge'), __('See which posts are missing translations, filtered by type or language.', 'langforge')); ?></h1>

            <!-- Filters -->
            <div style="margin:20px 0;padding:15px;background:#f5f5f5;display:flex;gap:15px;align-items:center;">
                <form method="get" style="display:flex;gap:10px;align-items:center;">
                    <input type="hidden" name="page" value="lf-status">
                    <label><?php esc_html_e('Post Type:', 'langforge'); ?></label>
                    <select name="filter_type">
                        <option value=""><?php esc_html_e('All', 'langforge'); ?></option>
                        <?php foreach ($translatable as $pt):
                            $pt_obj = get_post_type_object($pt);
                            if (!$pt_obj) continue;
                        ?>
                            <option value="<?php echo esc_attr($pt); ?>" <?php selected($filter_type, $pt); ?>>
                                <?php echo esc_html($pt_obj->labels->name); ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                    <label><?php esc_html_e('Missing translation for:', 'langforge'); ?></label>
                    <select name="lang">
                        <option value=""><?php esc_html_e('Any language', 'langforge'); ?></option>
                        <?php foreach ($languages as $code => $lang): ?>
                            <?php if ($code === $default_lang) continue; ?>
                            <option value="<?php echo esc_attr($code); ?>" <?php selected($filter_lang, $code); ?>>
                                <?php echo esc_html($lang['native_name']); ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                    <input type="submit" class="button" value="<?php esc_attr_e('Filter', 'langforge'); ?>">
                </form>
            </div>

            <!-- Progress overview -->
            <h2><?php esc_html_e('Overview', 'langforge'); ?></h2>
            <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:16px;margin-bottom:30px;">
            <?php
            $lf_table = $wpdb->prefix . 'lf_translations';
            $types_to_check = $filter_type ? [$filter_type] : $translatable;
            $type_placeholders = implode(',', array_fill(0, count($types_to_check), '%s'));

            // Source universe = PUBLISHED source posts only, to match the
            // Untranslated Content list and the Tools bulk-translate estimate
            // (both publish-only). Counting draft/pending source posts here made
            // the Overview report "Missing" items that never appeared in the list
            // and couldn't be bulk-translated (e.g. a leftover draft). A post
            // still counts as translated iff a translation relation exists in
            // lf_translations; the TARGET status drives the published/draft
            // breakdown below , so a draft *translation* still counts.
            // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core tables; IN built from %s placeholders, values are %-prepared.
            $total_count = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Status report count; computed on demand.
                "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p
                INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                WHERE p.post_status = 'publish'
                AND p.post_type IN ($type_placeholders)
                AND pm.meta_key = '_lf_language' AND pm.meta_value = %s",
                ...array_merge($types_to_check, [$default_lang])
            ));
            // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

            foreach ($languages as $code => $lang):
                if ($code === $default_lang) continue;

                $total = $total_count;

                // Count source posts that have a translation linked in
                // lf_translations for this language. Again, no post_status
                // filter on source or target — only existence of the pair.
                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core + $wpdb->prefix tables; IN built from %s placeholders, values are %-prepared.
                $translated = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $lf_table from $wpdb->prefix; values %-prepared.
                    "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p
                    INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                    INNER JOIN {$lf_table} t1 ON p.ID = t1.element_id
                    INNER JOIN {$lf_table} t2 ON t1.translation_group = t2.translation_group AND t2.language_code = %s
                    INNER JOIN {$wpdb->posts} tp ON t2.element_id = tp.ID AND tp.post_status NOT IN ('trash','auto-draft','inherit')
                    WHERE p.post_status = 'publish'
                    AND p.post_type IN ($type_placeholders)
                    AND pm.meta_key = '_lf_language' AND pm.meta_value = %s",
                    $code, ...array_merge($types_to_check, [$default_lang])
                ));
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

                // break translated count down by the target post's
                // publish status so the card can show published / draft /
                // missing instead of a single opaque percentage.
                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core + $wpdb->prefix tables; IN built from %s placeholders, values are %-prepared.
                $published = (int) $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $lf_table from $wpdb->prefix; values %-prepared.
                    "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p
                    INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                    INNER JOIN {$lf_table} t1 ON p.ID = t1.element_id
                    INNER JOIN {$lf_table} t2 ON t1.translation_group = t2.translation_group AND t2.language_code = %s
                    INNER JOIN {$wpdb->posts} tp ON t2.element_id = tp.ID AND tp.post_status = 'publish'
                    WHERE p.post_status = 'publish'
                    AND p.post_type IN ($type_placeholders)
                    AND pm.meta_key = '_lf_language' AND pm.meta_value = %s",
                    $code, ...array_merge($types_to_check, [$default_lang])
                ));
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $drafts  = max(0, $translated - $published);
                $missing = max(0, $total - $translated);

                // cast to int before comparing. round returns a
                // float, and `$pct === 100` was failing because
                // (float)100.0 !== (int)100 in strict mode — so a fully
                // translated language rendered with the warning (yellow)
                // colour instead of success (green).
                $pct = $total > 0 ? (int) round(($translated / $total) * 100) : 0;
                $color = $pct >= 100 ? '#00a32a' : ($pct > 50 ? '#dba617' : '#dc3232');
                $tooltip = sprintf(
                    /* translators: 1: published translation count, 2: draft count, 3: missing count. */
                    __('Published: %1$d · Draft: %2$d · Missing: %3$d', 'langforge'),
                    $published, $drafts, $missing
                );
            ?>
                <div style="background:#fff;border:1px solid #e0e0e0;padding:16px;border-radius:4px;" title="<?php echo esc_attr($tooltip); ?>">
                    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
                        <strong><?php echo esc_html($lang['native_name']); ?></strong>
                        <span style="font-size:20px;font-weight:600;color:<?php echo esc_attr($color); ?>"><?php echo esc_html($pct); ?>%</span>
                    </div>
                    <div style="height:8px;background:#e0e0e0;border-radius:4px;overflow:hidden;margin-bottom:8px;">
                        <div style="height:100%;width:<?php echo esc_attr($pct); ?>%;background:<?php echo esc_attr($color); ?>;border-radius:4px;"></div>
                    </div>
                    <small style="display:block;"><?php /* translators: 1: number of translated items, 2: total items. */ printf(esc_html__('%1$d of %2$d translated', 'langforge'), esc_html($translated), esc_html($total)); ?></small>
                    <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px;font-size:14px;">
                        <span style="color:#00a32a;">&#9679; <?php /* translators: %d: number of published translations. */ printf(esc_html__('Published %d', 'langforge'), esc_html($published)); ?></span>
                        <span style="color:#dba617;">&#9679; <?php /* translators: %d: number of draft translations. */ printf(esc_html__('Draft %d', 'langforge'), esc_html($drafts)); ?></span>
                        <span style="color:#94a3b8;">&#9679; <?php /* translators: %d: number of missing translations. */ printf(esc_html__('Missing %d', 'langforge'), esc_html($missing)); ?></span>
                    </div>
                </div>
            <?php endforeach; ?>
            </div>

            <?php
            // LF audit 2026-04-28 (L2): Translation Health tile.
            // Surfaces the number of translations whose source post has
            // changed since the translation was created (Content Diff
            // backend writes _lf_needs_update meta). Tile is clickable
            // when count > 0 — switches to ?view=outdated which renders
            // the out-of-date list further down. When count == 0 the
            // tile renders "all up to date" in calm green.
            $outdated_url_base = admin_url('admin.php?page=lf-status&view=outdated');
            if ($filter_type) $outdated_url_base = add_query_arg('filter_type', $filter_type, $outdated_url_base);
            // anchor so the click on the tile lands directly on
            // the list — not at the top of the Translation Status page,
            // which on a populated site means another scroll past the
            // language cards. scroll-margin-top on the H2 keeps it
            // breathing-room from the WP admin bar.
            $outdated_url_base .= '#lf-outdated-list';
            ?>
            <h2 style="margin-top:32px;"><?php esc_html_e('Translation Health', 'langforge'); ?></h2>
            <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:16px;margin-bottom:30px;">
                <?php if ($outdated_total > 0):
                    $tile_url = $filter_lang
                        ? add_query_arg('lang', $filter_lang, $outdated_url_base)
                        : $outdated_url_base;
                ?>
                    <a href="<?php echo esc_url($tile_url); ?>" class="lf-status-tile lf-status-tile--outdated"
                       style="display:block;text-decoration:none;color:inherit;background:#fff7ed;border:1px solid #fdba74;padding:16px;border-radius:6px;">
                        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
                            <strong style="color:#9a3412;"><?php esc_html_e('Out of Date', 'langforge'); ?></strong>
                            <span style="font-size:24px;font-weight:700;color:#c2410c;"><?php echo (int) $outdated_total; ?></span>
                        </div>
                        <small style="display:block;color:#7c2d12;">
                            <?php
                            printf(
                                /* translators: %d = number of translations whose source has changed */
                                esc_html(_n(
                                    '%d translation needs review after a source-content change.',
                                    '%d translations need review after source-content changes.',
                                    (int) $outdated_total,
                                    'langforge'
                                )),
                                (int) $outdated_total
                            );
                            ?>
                        </small>
                        <?php if (!empty($outdated_by_lang)):
                            $chips = [];
                            foreach ($outdated_by_lang as $code => $cnt) {
                                if ($cnt < 1) continue;
                                $chips[] = '<span style="color:#7c2d12;">&#9679; '
                                    . esc_html($languages[$code]['native_name']) . ' '
                                    . (int) $cnt
                                    . '</span>';
                            }
                            if ($chips):
                        ?>
                            <div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:8px;font-size:13px;">
                                <?php echo implode('', $chips); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built chip markup ?>
                            </div>
                        <?php endif; endif; ?>
                        <div style="margin-top:10px;color:#9a3412;font-size:13px;">
                            <?php esc_html_e('View list →', 'langforge'); ?>
                        </div>
                    </a>
                <?php else: ?>
                    <div class="lf-status-tile lf-status-tile--clean"
                         style="background:#ecfdf5;border:1px solid #a7f3d0;padding:16px;border-radius:6px;">
                        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
                            <strong style="color:#065f46;"><?php esc_html_e('Out of Date', 'langforge'); ?></strong>
                            <span style="font-size:24px;font-weight:700;color:#059669;">0</span>
                        </div>
                        <small style="display:block;color:#065f46;">
                            <?php esc_html_e('All translations are up to date with their source.', 'langforge'); ?>
                        </small>
                    </div>
                <?php endif; ?>
            </div>

            <?php if ($view === 'outdated'): ?>
                <!-- Out of Date Content list (LF audit L2) -->
                <h2 id="lf-outdated-list" style="margin-top:24px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;scroll-margin-top:64px;">
                    <?php esc_html_e('Out of Date Content', 'langforge'); ?>
                    <a href="<?php echo esc_url(admin_url('admin.php?page=lf-status')); ?>"
                       class="button button-small"
                       style="font-weight:400;">&larr; <?php esc_html_e('Back to Untranslated', 'langforge'); ?></a>
                </h2>
                <p class="description" style="margin-bottom:16px;">
                    <?php esc_html_e('These translations were flagged because their source post changed after the translation was last saved. Open each post to review the diff highlights and re-translate if needed.', 'langforge'); ?>
                </p>
                <?php
                // Build query for out-of-date posts.
                $outdated_args = [];
                $type_clause = '';
                if ($filter_type) {
                    $type_clause = ' AND p.post_type = %s';
                    $outdated_args[] = $filter_type;
                } else {
                    $placeholders = implode(',', array_fill(0, count($translatable), '%s'));
                    $type_clause = " AND p.post_type IN ($placeholders)";
                    $outdated_args = array_merge($outdated_args, $translatable);
                }
                $lang_clause = '';
                if ($filter_lang && $filter_lang !== $default_lang) {
                    $lang_clause = ' AND pm_lang.meta_value = %s';
                    $outdated_args[] = $filter_lang;
                } else {
                    $lang_clause = ' AND pm_lang.meta_value <> %s';
                    $outdated_args[] = $default_lang;
                }
                // surface the modified timestamp of both the
                // translation post itself and its source. JOIN to
                // wp_lf_translations twice — once to find the
                // translation_group of the outdated post, once to find
                // the default-language sibling in that group — and pull
                // its post_modified through a third join on wp_posts.
                // Outer LEFT JOINs because half-migrated rows
                // (translation in lf_translations but source row dropped)
                // shouldn't drop the outdated post from the list — we'd
                // rather show "—" than hide the row.
                $lf_table = $wpdb->prefix . 'lf_translations';
                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core + $wpdb->prefix tables; $type_clause/$lang_clause are literal %s placeholder fragments, values %-prepared below.
                $outdated_sql = "
                    SELECT p.ID, p.post_title, p.post_type, p.post_status,
                           p.post_date, p.post_modified,
                           pm_lang.meta_value AS lang_code,
                           pm_flag.meta_value AS flagged_at,
                           src.ID AS source_id,
                           src.post_modified AS source_modified
                    FROM {$wpdb->posts} p
                    INNER JOIN {$wpdb->postmeta} pm_flag
                        ON pm_flag.post_id = p.ID AND pm_flag.meta_key = '_lf_needs_update'
                    INNER JOIN {$wpdb->postmeta} pm_lang
                        ON pm_lang.post_id = p.ID AND pm_lang.meta_key = '_lf_language'
                    LEFT JOIN {$lf_table} t1 ON t1.element_id = p.ID
                    LEFT JOIN {$lf_table} t2 ON t2.translation_group = t1.translation_group
                        AND t2.language_code = %s
                    LEFT JOIN {$wpdb->posts} src ON src.ID = t2.element_id
                        AND src.post_status NOT IN ('trash','auto-draft','inherit')
                    WHERE p.post_status NOT IN ('trash','auto-draft','inherit')
                    {$type_clause}{$lang_clause}
                    ORDER BY pm_flag.meta_value DESC
                    LIMIT 200
                ";
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                // Inject the default-language code as the first %s for the
                // t2.language_code = %s placeholder above. Other args
                // (type clause, lang clause) follow in their declared order.
                $outdated_args = array_merge([$default_lang], $outdated_args);
                $outdated_posts = $wpdb->get_results($wpdb->prepare($outdated_sql, ...$outdated_args)); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $outdated_sql is prepared here with %s placeholders; core/own tables.
                if (empty($outdated_posts)):
                    echo '<p style="color:#059669;font-weight:600;">' . esc_html__('Nothing flagged. Translations are current with their source.', 'langforge') . '</p>';
                else:
                ?>
                    <?php
                    // Original modified + Translation modified
                    // formatter — returns pre-escaped HTML <span> with a
                    // tooltip carrying the relative time.
                    $fmt_ts = function ($mysql_dt) {
                        if (empty($mysql_dt) || $mysql_dt === '0000-00-00 00:00:00') return '&mdash;';
                        $ts = strtotime($mysql_dt);
                        if (!$ts) return esc_html($mysql_dt);
                        $now = current_time('timestamp');
                        $abs = date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $ts);
                        $rel = $ts > $now
                            /* translators: %s: human-readable time difference (e.g. "5 minutes"). */
                            ? sprintf(__('in %s', 'langforge'), human_time_diff($now, $ts))
                            /* translators: %s: human-readable time difference (e.g. "5 minutes"). */
                            : sprintf(__('%s ago', 'langforge'), human_time_diff($ts, $now));
                        return '<span title="' . esc_attr($rel) . '">' . esc_html($abs) . '</span>';
                    };
                    ?>
                    <table class="widefat striped lf-outdated-table">
                        <thead>
                            <tr>
                                <th><?php esc_html_e('Title', 'langforge'); ?></th>
                                <th><?php esc_html_e('Type', 'langforge'); ?></th>
                                <th><?php esc_html_e('Language pair', 'langforge'); ?></th>
                                <th><?php esc_html_e('Original modified', 'langforge'); ?></th>
                                <th><?php esc_html_e('Translation modified', 'langforge'); ?></th>
                                <th><?php esc_html_e('Flagged', 'langforge'); ?></th>
                                <th></th>
                            </tr>
                        </thead>
                        <tbody>
                        <?php foreach ($outdated_posts as $post):
                            $lang_meta  = $post->lang_code;
                            $lang_label = isset($languages[$lang_meta]['native_name'])
                                ? $languages[$lang_meta]['native_name']
                                : strtoupper($lang_meta);
                            $src_label  = isset($languages[$default_lang]['native_name'])
                                ? $languages[$default_lang]['native_name']
                                : strtoupper($default_lang);
                            $flagged_ts = strtotime($post->flagged_at);
                            $source_id  = (int) ($post->source_id ?? 0);
                        ?>
                            <tr data-post-id="<?php echo (int) $post->ID; ?>"
                                data-source-id="<?php echo esc_attr($source_id); ?>"
                                data-source-lang="<?php echo esc_attr($default_lang); ?>"
                                data-target-lang="<?php echo esc_attr($lang_meta); ?>">
                                <td>
                                    <a href="<?php echo esc_url(get_edit_post_link($post->ID)); ?>">
                                        <?php echo esc_html($post->post_title ?: __('(no title)', 'langforge')); ?>
                                    </a>
                                </td>
                                <td><?php echo esc_html($post->post_type); ?></td>
                                <td>
                                    <span style="color:#64748b;"><?php echo esc_html($src_label); ?></span>
                                    &rarr;
                                    <strong><?php echo esc_html($lang_label); ?></strong>
                                </td>
                                <td><?php echo $fmt_ts($post->source_modified ?? null); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- closure returns pre-escaped markup ?></td>
                                <td><?php echo $fmt_ts($post->post_modified ?? null);   // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- closure returns pre-escaped markup ?></td>
                                <td><?php
                                    // R2-7: if a third party (CLI / restore /
                                    // legacy plugin) wrote a non-date value
                                    // to _lf_needs_update, render the raw
                                    // value with an "(unknown date)" hint
                                    // instead of an empty em-dash so the
                                    // user has something to grep on.
                                    if ($flagged_ts) {
                                        echo esc_html(human_time_diff($flagged_ts, current_time('timestamp')) . ' ' . __('ago', 'langforge'));
                                    } elseif (!empty($post->flagged_at)) {
                                        echo esc_html(sprintf(
                                            /* translators: %s = raw flag value */
                                            __('flagged (%s)', 'langforge'),
                                            $post->flagged_at
                                        ));
                                    } else {
                                        echo '&mdash;';
                                    }
                                ?></td>
                                <td style="text-align:right;white-space:nowrap;">
                                    <?php if ($source_id > 0): ?>
                                        <?php $rt_cost = class_exists('Forge_Credits_Widget') ? (int) Forge_Credits_Widget::get_action_cost('translate_post') : 3; if ($rt_cost < 1) { $rt_cost = 1; } ?>
                                        <button type="button" class="button button-small button-primary lf-outdated-retranslate"
                                                title="<?php /* translators: %d: approximate number of AI credits the re-translation will use. */ echo esc_attr(sprintf(__('Re-run AI translation against the latest source content — uses about %d AI credits', 'langforge'), $rt_cost)); ?>"
                                                style="display:inline-flex;align-items:center;gap:5px;">
                                            <?php esc_html_e('Re-translate', 'langforge'); ?>
                                            <span style="display:inline-flex;align-items:center;gap:3px;background:rgba(255,255,255,.22);border-radius:9px;padding:0 7px;font-size:11px;font-weight:700;line-height:1.7;">
                                                <svg viewBox="0 0 24 22" width="11" height="10" fill="currentColor" aria-hidden="true"><polygon points="7,4 17,4 21,9 12,19 3,9"/></svg><?php echo (int) $rt_cost; ?>
                                            </span>
                                        </button>
                                    <?php endif; ?>
                                    <a class="button button-small" href="<?php echo esc_url(get_edit_post_link($post->ID)); ?>">
                                        <?php esc_html_e('Review', 'langforge'); ?>
                                    </a>
                                </td>
                            </tr>
                        <?php endforeach; ?>
                        </tbody>
                    </table>
                    <script>
 // wire per-row Re-translate to the existing
                    // lf_ai_translate_post AJAX. On success the PHP
                    // handler clears _lf_needs_update + refreshes the
                    // source snapshot, so we fade out the row. textContent
                    // only — no innerHTML — keeps this XSS-safe.
                    (function() {
                        var nonce      = <?php echo wp_json_encode(wp_create_nonce('lf_admin_nonce')); ?>;
                        var ajaxUrl    = <?php echo wp_json_encode(admin_url('admin-ajax.php')); ?>;
                        var labelDef   = <?php echo wp_json_encode(__('Re-translate', 'langforge')); ?>;
                        var labelBusy  = <?php echo wp_json_encode(__('Translating…', 'langforge')); ?>;
                        var failMsg    = <?php echo wp_json_encode(__('Re-translate failed.', 'langforge')); ?>;
                        var netMsg     = <?php echo wp_json_encode(__('Network error during re-translate.', 'langforge')); ?>;
                        var confirmMsg = <?php /* translators: %d: approximate number of AI credits the re-translation will use. */ echo wp_json_encode(sprintf(__('Re-run AI translation for this post? This uses about %d AI credits and overwrites the current translation.', 'langforge'), (int) (class_exists('Forge_Credits_Widget') ? Forge_Credits_Widget::get_action_cost('translate_post') : 3))); ?>;
                        document.querySelectorAll('.lf-outdated-retranslate').forEach(function(btn) {
                            btn.addEventListener('click', function() {
                                var row = btn.closest('tr');
                                if (!row) return;
                                var sourceId   = row.getAttribute('data-source-id') || 0;
                                var targetId   = row.getAttribute('data-post-id') || 0;
                                var sourceLang = row.getAttribute('data-source-lang') || '';
                                var targetLang = row.getAttribute('data-target-lang') || '';
                                if (!sourceId || !targetId || !sourceLang || !targetLang) return;
                                if (!confirm(confirmMsg)) return;

                                btn.disabled = true;
                                btn.textContent = labelBusy;

                                var fd = new FormData();
                                fd.append('action', 'lf_ai_translate_post');
                                fd.append('nonce', nonce);
                                fd.append('source_id', sourceId);
                                fd.append('target_id', targetId);
                                fd.append('source_lang', sourceLang);
                                fd.append('target_lang', targetLang);

                                fetch(ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' })
                                    .then(function(r){ return r.json(); })
                                    .then(function(j){
                                        if (j && j.success) {
                                            row.style.transition = 'opacity .35s';
                                            row.style.opacity = '0.3';
                                            setTimeout(function(){ row.parentNode && row.parentNode.removeChild(row); }, 400);
                                        } else {
                                            btn.disabled = false;
                                            btn.textContent = labelDef;
                                            var msg = (j && j.data && j.data.message) || failMsg;
                                            alert(msg);
                                        }
                                    })
                                    .catch(function(){
                                        btn.disabled = false;
                                        btn.textContent = labelDef;
                                        alert(netMsg);
                                    });
                            });
                        });
                    })();
                    </script>
                <?php endif; ?>
            <?php else: ?>
            <!-- Untranslated posts list -->
            <h2><?php esc_html_e('Untranslated Content', 'langforge'); ?></h2>
            <?php
            $other_langs = array_diff_key($languages, [$default_lang => true]);
            if (!empty($other_langs)):
                $lf_table = $wpdb->prefix . 'lf_translations';

                // Get all published posts in default language (with optional post_type filter)
                $type_clause = '';
                $query_args = [$default_lang];
                if ($filter_type) {
                    $type_clause = ' AND p.post_type = %s';
                    $query_args[] = $filter_type;
                } else {
                    // Limit to translatable post types only
                    $placeholders = implode(',', array_fill(0, count($translatable), '%s'));
                    $type_clause = " AND p.post_type IN ($placeholders)";
                    $query_args = array_merge($query_args, $translatable);
                }
                // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Core tables; $type_clause is a literal %s placeholder fragment, values %-prepared below.
                $all_posts = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $type_clause is %s placeholders; values %-prepared.
                    "SELECT p.ID, p.post_title, p.post_type, p.post_date
                    FROM {$wpdb->posts} p
                    INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                    WHERE p.post_status = 'publish'
                    AND pm.meta_key = '_lf_language' AND pm.meta_value = %s
                    {$type_clause}
                    ORDER BY p.post_date DESC
                    LIMIT 100",
                    ...$query_args
                ));
                // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

                // For each post, find which languages are missing
                $untranslated = [];
                foreach ($all_posts as $post) {
                    $existing = LF_Post_Translation::instance()->get_translations($post->ID);
                    $missing = [];
                    $langs_to_check = $filter_lang ? [$filter_lang => $other_langs[$filter_lang] ?? null] : $other_langs;
                    $langs_to_check = array_filter($langs_to_check);
                    foreach ($langs_to_check as $code => $lang) {
                        if (!isset($existing[$code])) {
                            $missing[$code] = $lang;
                        }
                    }
                    if (!empty($missing)) {
                        $post->missing_langs = $missing;
                        $untranslated[] = $post;
                    }
                }

                if (empty($untranslated)):
                    echo '<p style="color:#059669;font-weight:600;">' . esc_html__('All content is translated!', 'langforge') . '</p>';
                else:
            ?>
                <table class="widefat striped">
                    <thead>
                        <tr>
                            <th><?php esc_html_e('Title', 'langforge'); ?></th>
                            <th><?php esc_html_e('Type', 'langforge'); ?></th>
                            <th><?php esc_html_e('Date', 'langforge'); ?></th>
                            <th><?php esc_html_e('Missing Translations', 'langforge'); ?></th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php foreach ($untranslated as $post): ?>
                        <tr>
                            <td>
                                <a href="<?php echo esc_url(get_edit_post_link($post->ID)); ?>">
                                    <?php echo esc_html($post->post_title); ?>
                                </a>
                            </td>
                            <td><?php echo esc_html($post->post_type); ?></td>
                            <td><?php echo esc_html(date_i18n(get_option('date_format'), strtotime($post->post_date))); ?></td>
                            <td>
                                <div style="display:flex;gap:4px;flex-wrap:wrap;">
                                <?php foreach ($post->missing_langs as $code => $lang): ?>
                                    <button type="button" class="button button-small lf-create-translation"
                                            data-post-id="<?php echo esc_attr($post->ID); ?>"
                                            data-lang="<?php echo esc_attr($code); ?>"
                                            style="font-size:14px;padding:2px 8px;">
                                        + <?php echo esc_html(strtoupper($code)); ?>
                                    </button>
                                <?php endforeach; ?>
                                </div>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            <?php
                endif;
            endif;
            ?>
            <?php endif; // end of view==='outdated' / else branch (LF audit L2) ?>
        </div>
        <?php
    }

    /**
 * Render Glossary admin page
 */
    public function render_glossary_page() {
        $languages = LF_Core::instance()->get_active_languages();
        $default_lang = LF_Core::instance()->get_default_language();
        $from = isset($_GET['from']) ? sanitize_text_field(wp_unslash($_GET['from'])) : $default_lang;
        $to = isset($_GET['to']) ? sanitize_text_field(wp_unslash($_GET['to'])) : '';

        if (!$to) {
            foreach ($languages as $code => $lang) {
                if ($code !== $default_lang) { $to = $code; break; }
            }
        }

        // Handle add term
        if (isset($_POST['lf_add_glossary_term']) && wp_verify_nonce(isset($_POST['lf_glossary_nonce']) ? sanitize_text_field(wp_unslash($_POST['lf_glossary_nonce'])) : '', 'lf_add_glossary')) {
            // Round 2 audit fix (R2-3 + R2-4): validate before insert so the
            // user sees a specific error instead of a silent success notice
            // hiding either an empty row (HTML-only input stripped to '')
            // or a truncated row (varchar(255) silent truncation).
            $raw_source = isset($_POST['source_term']) ? wp_unslash($_POST['source_term']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below via wp_strip_all_tags + length validation.
            $raw_trans  = isset($_POST['translated_term']) ? wp_unslash($_POST['translated_term']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below via wp_strip_all_tags + length validation.
            // Strip all tags first — sanitize_text_field would leave the
            // text content of <script> intact, but wp_strip_all_tags also
            // removes script bodies, which is what we want here (R2-4).
            $clean_source = trim(wp_strip_all_tags($raw_source, true));
            $clean_trans  = trim(wp_strip_all_tags($raw_trans, true));

            $errors = [];
            if ($clean_source === '') {
                // R2-4: HTML-only input strips to nothing. Tell the user.
                $errors[] = __('Source term cannot be empty (HTML and script tags are removed).', 'langforge');
            }
            if (mb_strlen($clean_source) > 255) {
                // R2-3: silent truncation at varchar(255) — surface it.
                $errors[] = sprintf(
                    /* translators: %d = max characters */
                    __('Source term is too long (max %d characters).', 'langforge'),
                    255
                );
            }
            if (mb_strlen($clean_trans) > 255) {
                $errors[] = sprintf(
                    /* translators: %d = max characters */
                    __('Translation is too long (max %d characters).', 'langforge'),
                    255
                );
            }

            if ($errors) {
                echo '<div class="notice notice-error"><p>' . esc_html(implode(' ', $errors)) . '</p></div>';
            } else {
                LF_Glossary::instance()->add_term(
                    $clean_source, $clean_trans, $from, $to,
                    [
                        'description' => sanitize_textarea_field(wp_unslash($_POST['description'] ?? '')),
                        'do_not_translate' => !empty($_POST['do_not_translate']),
                        // LF audit 2026-04-28 (L3): expose case_sensitive in UI.
                        // DB column + REST endpoint already accepted it; only
                        // the form field was missing.
                        'case_sensitive' => !empty($_POST['case_sensitive']),
                    ]
                );
                echo '<div class="notice notice-success"><p>' . esc_html__('Term added.', 'langforge') . '</p></div>';
            }
        }

        // Handle CSV import (LF audit 2026-04-28: USER_GUIDE promised
        // an Import CSV button on this page; backend was already there
        // via REST, only the UI form was missing).
        if (isset($_POST['lf_glossary_import']) && wp_verify_nonce(isset($_POST['lf_glossary_nonce']) ? sanitize_text_field(wp_unslash($_POST['lf_glossary_nonce'])) : '', 'lf_add_glossary')) {
            if (!empty($_FILES['glossary_csv']['tmp_name']) && is_uploaded_file($_FILES['glossary_csv']['tmp_name'])) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- $_FILES path validated by is_uploaded_file.
                $csv = file_get_contents($_FILES['glossary_csv']['tmp_name']); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- $_FILES path validated by is_uploaded_file above.
                $count = LF_Glossary::instance()->import_csv($csv, $from, $to);
                echo '<div class="notice notice-success"><p>' . sprintf(
                    /* translators: %d = number of glossary terms imported */
                    esc_html__('Imported %d term(s).', 'langforge'), (int) $count
                ) . '</p></div>';
            } else {
                echo '<div class="notice notice-error"><p>' . esc_html__('No CSV file uploaded.', 'langforge') . '</p></div>';
            }
        }

        // Note: export is handled earlier on admin_init via
        // maybe_handle_glossary_export so we can stream the file
        // before WP admin chrome echoes any HTML. By the time
        // render_glossary_page runs, an export request would already
        // have hit exit.

        $terms = LF_Glossary::instance()->get_terms($from, $to);
        ?>
        <div class="wrap">
            <?php LF_Admin_Tabs::render('lf-glossary'); ?>
            <?php LF_License::instance()->render_upgrade_banner(); ?>
            <h1><?php esc_html_e('Glossary', 'langforge'); ?><?php Forge_Docs_Link::render('langforge', 'glossary-keeping-terminology-consistent-pro', __('Glossary', 'langforge'), __('Lock brand terms so AI translation keeps them consistent.', 'langforge')); ?></h1>
            <p class="description"><?php esc_html_e('Define brand terminology to ensure translation consistency.', 'langforge'); ?></p>

            <!-- Language pair selector -->
            <form method="get" style="margin:20px 0;display:flex;gap:10px;align-items:center;">
                <input type="hidden" name="page" value="lf-glossary">
                <label><strong><?php esc_html_e('From:', 'langforge'); ?></strong></label>
                <select name="from">
                    <?php foreach ($languages as $code => $lang): ?>
                        <option value="<?php echo esc_attr($code); ?>" <?php selected($from, $code); ?>><?php echo esc_html($lang['native_name']); ?></option>
                    <?php endforeach; ?>
                </select>
                <label><strong><?php esc_html_e('To:', 'langforge'); ?></strong></label>
                <select name="to">
                    <?php foreach ($languages as $code => $lang): ?>
                        <option value="<?php echo esc_attr($code); ?>" <?php selected($to, $code); ?>><?php echo esc_html($lang['native_name']); ?></option>
                    <?php endforeach; ?>
                </select>
                <input type="submit" class="button" value="<?php esc_attr_e('Filter', 'langforge'); ?>">
            </form>

            <!-- Terms table -->
            <table class="widefat striped" style="max-width:980px;" id="lf-glossary-table">
                <thead>
                    <tr>
                        <th><?php esc_html_e('Source Term', 'langforge'); ?></th>
                        <th><?php esc_html_e('Translation', 'langforge'); ?></th>
                        <th><?php esc_html_e('Description', 'langforge'); ?></th>
                        <th><?php esc_html_e('Do Not Translate', 'langforge'); ?></th>
                        <th title="<?php esc_attr_e('When ON, matching is exact (Foo ≠ foo). When OFF, any casing matches.', 'langforge'); ?>"><?php esc_html_e('Case Sensitive', 'langforge'); ?></th>
                        <th style="width:120px;"><?php esc_html_e('Actions', 'langforge'); ?></th>
                    </tr>
                </thead>
                <tbody>
                <?php if (empty($terms)): ?>
                    <tr class="lf-glossary-empty"><td colspan="6"><?php esc_html_e('No glossary terms yet.', 'langforge'); ?></td></tr>
                <?php endif; ?>
                <?php foreach ($terms as $term): ?>
                    <tr data-id="<?php echo esc_attr($term->id); ?>">
                        <td class="lf-gl-source"><strong><?php echo esc_html($term->source_term); ?></strong></td>
                        <td class="lf-gl-translated"><?php echo esc_html($term->translated_term); ?></td>
                        <td class="lf-gl-desc"><small><?php echo esc_html($term->description); ?></small></td>
                        <td class="lf-gl-dnt"><?php echo $term->do_not_translate ? '&#10003;' : ''; ?></td>
                        <td class="lf-gl-cs"><?php echo $term->case_sensitive ? '&#10003;' : ''; ?></td>
                        <td>
                            <button type="button" class="button button-small lf-gl-edit"><?php esc_html_e('Edit', 'langforge'); ?></button>
                            <button type="button" class="button button-small lf-gl-delete" style="color:#dc2626;"><?php esc_html_e('Delete', 'langforge'); ?></button>
                        </td>
                    </tr>
                <?php endforeach; ?>
                </tbody>
            </table>

            <script>
            jQuery(function($) {
                var restBase = '<?php echo esc_url(rest_url('langforge/v1/glossary')); ?>';
                var nonce = '<?php echo esc_html(wp_create_nonce('wp_rest')); ?>';

                // Delete term
                $('#lf-glossary-table').on('click', '.lf-gl-delete', function() {
                    var $row = $(this).closest('tr');
                    var id = $row.data('id');
                    if (!confirm('<?php echo esc_js(__('Delete this glossary term?', 'langforge')); ?>')) return;

                    $.ajax({
                        url: restBase + '/' + id,
                        method: 'DELETE',
                        beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', nonce); }
                    }).done(function() {
                        $row.fadeOut(300, function() { $(this).remove(); });
                    });
                });

                // Edit term — convert row to inline editing
                $('#lf-glossary-table').on('click', '.lf-gl-edit', function() {
                    var $row = $(this).closest('tr');
                    var id = $row.data('id');
                    var $src = $row.find('.lf-gl-source');
                    var $tr = $row.find('.lf-gl-translated');
                    var $desc = $row.find('.lf-gl-desc');
                    var $dnt = $row.find('.lf-gl-dnt');
                    var $cs = $row.find('.lf-gl-cs');

                    // Save original values for cancel
                    var origSource = $src.text().trim();
                    var origTranslated = $tr.text().trim();
                    var origDesc = $desc.text().trim();
                    var origDnt = $dnt.text().trim() === '\u2713';
                    var origCs = $cs.text().trim() === '\u2713';

                    // Replace with inputs
                    $src.html('<input type="text" class="regular-text" value="' + $('<span>').text(origSource).html() + '" style="width:100%;">');
                    $tr.html('<input type="text" class="regular-text" value="' + $('<span>').text(origTranslated).html() + '" style="width:100%;">');
                    $desc.html('<input type="text" class="regular-text" value="' + $('<span>').text(origDesc).html() + '" style="width:100%;">');
                    $dnt.html('<input type="checkbox"' + (origDnt ? ' checked' : '') + '>');
                    $cs.html('<input type="checkbox"' + (origCs ? ' checked' : '') + '>');

                    // Change buttons
                    $row.find('td:last').html(
                        '<button type="button" class="button button-small button-primary lf-gl-save"><?php echo esc_js(__('Save', 'langforge')); ?></button> ' +
                        '<button type="button" class="button button-small lf-gl-cancel"><?php echo esc_js(__('Cancel', 'langforge')); ?></button>'
                    );
                });

                // Save edited term
                $('#lf-glossary-table').on('click', '.lf-gl-save', function() {
                    var $row = $(this).closest('tr');
                    var id = $row.data('id');
                    var data = {
                        source_term: $row.find('.lf-gl-source input').val(),
                        translated_term: $row.find('.lf-gl-translated input').val(),
                        description: $row.find('.lf-gl-desc input').val(),
                        do_not_translate: $row.find('.lf-gl-dnt input').is(':checked') ? 1 : 0,
                        case_sensitive: $row.find('.lf-gl-cs input').is(':checked') ? 1 : 0
                    };

                    $.ajax({
                        url: restBase + '/' + id,
                        method: 'PUT',
                        contentType: 'application/json',
                        data: JSON.stringify(data),
                        beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', nonce); }
                    }).done(function() {
                        $row.find('.lf-gl-source').html('<strong>' + $('<span>').text(data.source_term).html() + '</strong>');
                        $row.find('.lf-gl-translated').text(data.translated_term);
                        $row.find('.lf-gl-desc').html('<small>' + $('<span>').text(data.description).html() + '</small>');
                        $row.find('.lf-gl-dnt').html(data.do_not_translate ? '&#10003;' : '');
                        $row.find('.lf-gl-cs').html(data.case_sensitive ? '&#10003;' : '');
                        $row.find('td:last').html(
                            '<button type="button" class="button button-small lf-gl-edit"><?php echo esc_js(__('Edit', 'langforge')); ?></button> ' +
                            '<button type="button" class="button button-small lf-gl-delete" style="color:#dc2626;"><?php echo esc_js(__('Delete', 'langforge')); ?></button>'
                        );
                    });
                });

                // Cancel editing
                $('#lf-glossary-table').on('click', '.lf-gl-cancel', function() {
                    location.reload();
                });
            });
            </script>

            <!-- Add term form -->
            <div class="avl-card" style="margin-top:24px;">
                <h3 style="margin-top:0;"><?php esc_html_e('Add Term', 'langforge'); ?></h3>
                <form method="post">
                    <?php wp_nonce_field('lf_add_glossary', 'lf_glossary_nonce'); ?>
                    <table class="form-table">
                        <tr>
                            <th><label><?php esc_html_e('Source Term', 'langforge'); ?></label></th>
                            <td><input type="text" name="source_term" class="regular-text" required></td>
                        </tr>
                        <tr>
                            <th><label><?php esc_html_e('Translation', 'langforge'); ?></label></th>
                            <td><input type="text" name="translated_term" class="regular-text"></td>
                        </tr>
                        <tr>
                            <th><label><?php esc_html_e('Description', 'langforge'); ?></label></th>
                            <td><input type="text" name="description" class="regular-text"></td>
                        </tr>
                        <tr>
                            <th></th>
                            <td><label><input type="checkbox" name="do_not_translate" value="1"> <?php esc_html_e('Do not translate (keep as-is)', 'langforge'); ?></label></td>
                        </tr>
                        <tr>
                            <th></th>
                            <td>
                                <label>
                                    <input type="checkbox" name="case_sensitive" value="1">
                                    <?php esc_html_e('Case sensitive', 'langforge'); ?>
                                </label>
                                <p class="description" style="margin:6px 0 0;max-width:520px;">
                                    <?php esc_html_e('When ON, only the exact casing matches (e.g. "iPhone" matches but "iphone" does not). When OFF, the term is matched regardless of casing. Most brand terms benefit from case-sensitive matching.', 'langforge'); ?>
                                </p>
                            </td>
                        </tr>
                    </table>
                    <p><input type="submit" name="lf_add_glossary_term" class="button button-primary" value="<?php esc_attr_e('Add Term', 'langforge'); ?>"></p>
                </form>
            </div>

            <!-- Import / Export CSV (LF audit 2026-04-28). Backend
 import_csv / export_csv lived in LF_Glossary all along;
 only the UI was missing. The form posts to the same page
 so the upload+download handler at the top of
 render_glossary_page catches it. -->
            <div class="avl-card" style="margin-top:24px;">
                <h3 style="margin-top:0;"><?php esc_html_e('Import / Export', 'langforge'); ?></h3>
                <p class="description" style="margin-bottom:16px;">
                    <?php printf(
                        /* translators: %1$s = source language, %2$s = target language */
                        esc_html__('CSV operations apply to the current language pair: %1$s → %2$s. Change the pair above before importing or exporting.', 'langforge'),
                        '<strong>' . esc_html($languages[$from]['native_name'] ?? $from) . '</strong>',
                        '<strong>' . esc_html($languages[$to]['native_name'] ?? $to) . '</strong>'
                    ); ?>
                </p>
                <p class="description" style="margin-bottom:16px;">
                    <?php esc_html_e('CSV columns: source_term, translated_term, description, do_not_translate (0/1), case_sensitive (0/1). The first row may be a header. Older 4-column files (without case_sensitive) are still accepted — they import with case_sensitive=0.', 'langforge'); ?>
                </p>
                <div style="display:flex;gap:24px;flex-wrap:wrap;align-items:flex-start;">
                    <form method="post" enctype="multipart/form-data" style="flex:0 0 auto;">
                        <?php wp_nonce_field('lf_add_glossary', 'lf_glossary_nonce'); ?>
                        <input type="hidden" name="from" value="<?php echo esc_attr($from); ?>">
                        <input type="hidden" name="to" value="<?php echo esc_attr($to); ?>">
                        <p style="margin-top:0;">
                            <input type="file" name="glossary_csv" accept=".csv,text/csv" required>
                            <input type="submit" name="lf_glossary_import" class="button" value="<?php esc_attr_e('Import CSV', 'langforge'); ?>">
                        </p>
                    </form>
                    <form method="post" style="flex:0 0 auto;">
                        <?php wp_nonce_field('lf_add_glossary', 'lf_glossary_nonce'); ?>
                        <input type="hidden" name="from" value="<?php echo esc_attr($from); ?>">
                        <input type="hidden" name="to" value="<?php echo esc_attr($to); ?>">
                        <p style="margin-top:0;">
                            <input type="submit" name="lf_glossary_export" class="button" value="<?php esc_attr_e('Export CSV', 'langforge'); ?>">
                        </p>
                    </form>
                </div>
            </div>
        </div>
        <?php
    }

    /**
 * Check if WPML data exists
 */
    private function wpml_data_exists() {
        global $wpdb;
        $table = $wpdb->prefix . 'icl_translations';
        return $wpdb->get_var("SHOW TABLES LIKE '$table'") === $table; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- WPML legacy table from $wpdb->prefix; existence check
    }
    
    /**
 * Migrate WPML translation data to our plugin
 */
    private function migrate_wpml_data() {
        global $wpdb;
        
        $icl_translations = $wpdb->prefix . 'icl_translations';
        $lf_translations = $wpdb->prefix . 'lf_translations';
        
        // Check if WPML table exists
        if ($wpdb->get_var("SHOW TABLES LIKE '$icl_translations'") !== $icl_translations) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- WPML legacy table from $wpdb->prefix; existence check
            return ['success' => false, 'error' => 'WPML translations table not found'];
        }

        // Get all translation groups from WPML
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- WPML legacy table from $wpdb->prefix; one-time migration read
        $wpml_data = $wpdb->get_results("
            SELECT element_id, element_type, trid, language_code, source_language_code
            FROM $icl_translations
            WHERE element_type LIKE 'post_%'
            ORDER BY trid, language_code
        ");
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        
        if (empty($wpml_data)) {
            return ['success' => false, 'error' => 'No WPML translation data found'];
        }
        
        $imported = 0;
        $groups_imported = [];
        
        foreach ($wpml_data as $row) {
            // Extract post ID from element_id
            $post_id = intval($row->element_id);
            if (!$post_id) continue;
            
            // Check if post exists
            if (!get_post($post_id)) continue;
            
            // Use WPML's trid as our translation_group
            $translation_group = intval($row->trid);
            $language_code = sanitize_text_field($row->language_code);
            $source_language = $row->source_language_code ? sanitize_text_field($row->source_language_code) : null;
            
            // Extract element type (post, page, etc.)
            $element_type = str_replace('post_', '', $row->element_type);
            
            // Check if already exists
            $exists = $wpdb->get_var($wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- own table from $wpdb->prefix; one-time migration read
                "SELECT id FROM $lf_translations WHERE element_id = %d AND element_type = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from $wpdb->prefix, values are %-prepared.
                $post_id, $element_type
            ));

            if (!$exists) {
                $wpdb->insert($lf_translations, [ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- own table from $wpdb->prefix; one-time migration write
                    'element_id' => $post_id,
                    'element_type' => $element_type,
                    'translation_group' => $translation_group,
                    'language_code' => $language_code,
                    'source_language_code' => $source_language
                ]);
                
                // Also save language as post meta for quick access
                update_post_meta($post_id, '_lf_language', $language_code);
                update_post_meta($post_id, '_lf_translation_group', $translation_group);
                
                $imported++;
                $groups_imported[$translation_group] = true;
            }
        }
        
        // Import active languages from WPML
        $wpml_languages = $wpdb->prefix . 'icl_languages';
        if ($wpdb->get_var("SHOW TABLES LIKE '$wpml_languages'") === $wpml_languages) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- WPML legacy table from $wpdb->prefix; existence check
            $active_langs = $wpdb->get_results("SELECT code, active FROM $wpml_languages WHERE active = 1"); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- WPML legacy table from $wpdb->prefix; one-time migration read
            if ($active_langs) {
                $all_languages = LF_Core::instance()->get_available_languages();
                $active = [];
                foreach ($active_langs as $lang) {
                    $code = $lang->code;
                    if (isset($all_languages[$code])) {
                        $active[$code] = $all_languages[$code];
                    }
                }
                if (!empty($active)) {
                    update_option('lf_active_languages', $active);
                }
            }
        }
        
        return ['success' => true, 'count' => count($groups_imported)];
    }

    /**
 * Convert country code to flag emoji
 */
    public static function get_flag_emoji($code) {
        $map = self::get_flag_map();
        return $map[$code] ?? '';
    }

    /**
 * Map of country/flag codes to emoji flags
 */
    public static function get_flag_map() {
        // Flag emoji = Regional Indicator A(\xA6) … Z(\xBF), prefix \xF0\x9F\x87
        return [
            // Already existing
            'ru' => "\xF0\x9F\x87\xB7\xF0\x9F\x87\xBA", // RU
            'gb' => "\xF0\x9F\x87\xAC\xF0\x9F\x87\xA7", // GB
            'us' => "\xF0\x9F\x87\xBA\xF0\x9F\x87\xB8", // US
            'de' => "\xF0\x9F\x87\xA9\xF0\x9F\x87\xAA", // DE
            'fr' => "\xF0\x9F\x87\xAB\xF0\x9F\x87\xB7", // FR
            'es' => "\xF0\x9F\x87\xAA\xF0\x9F\x87\xB8", // ES
            'it' => "\xF0\x9F\x87\xAE\xF0\x9F\x87\xB9", // IT
            'pt' => "\xF0\x9F\x87\xB5\xF0\x9F\x87\xB9", // PT
            'br' => "\xF0\x9F\x87\xA7\xF0\x9F\x87\xB7", // BR
            'nl' => "\xF0\x9F\x87\xB3\xF0\x9F\x87\xB1", // NL
            'cn' => "\xF0\x9F\x87\xA8\xF0\x9F\x87\xB3", // CN
            'tw' => "\xF0\x9F\x87\xB9\xF0\x9F\x87\xBC", // TW
            'jp' => "\xF0\x9F\x87\xAF\xF0\x9F\x87\xB5", // JP
            'kr' => "\xF0\x9F\x87\xB0\xF0\x9F\x87\xB7", // KR
            'sa' => "\xF0\x9F\x87\xB8\xF0\x9F\x87\xA6", // SA
            'il' => "\xF0\x9F\x87\xAE\xF0\x9F\x87\xB1", // IL
            'tr' => "\xF0\x9F\x87\xB9\xF0\x9F\x87\xB7", // TR
            'pl' => "\xF0\x9F\x87\xB5\xF0\x9F\x87\xB1", // PL
            'ua' => "\xF0\x9F\x87\xBA\xF0\x9F\x87\xA6", // UA
            'se' => "\xF0\x9F\x87\xB8\xF0\x9F\x87\xAA", // SE
            'no' => "\xF0\x9F\x87\xB3\xF0\x9F\x87\xB4", // NO
            'dk' => "\xF0\x9F\x87\xA9\xF0\x9F\x87\xB0", // DK
            'fi' => "\xF0\x9F\x87\xAB\xF0\x9F\x87\xAE", // FI
            'is' => "\xF0\x9F\x87\xAE\xF0\x9F\x87\xB8", // IS
            'cz' => "\xF0\x9F\x87\xA8\xF0\x9F\x87\xBF", // CZ
            'sk' => "\xF0\x9F\x87\xB8\xF0\x9F\x87\xB0", // SK
            'si' => "\xF0\x9F\x87\xB8\xF0\x9F\x87\xAE", // SI
            'hr' => "\xF0\x9F\x87\xAD\xF0\x9F\x87\xB7", // HR
            'ba' => "\xF0\x9F\x87\xA7\xF0\x9F\x87\xA6", // BA
            'rs' => "\xF0\x9F\x87\xB7\xF0\x9F\x87\xB8", // RS
            'mk' => "\xF0\x9F\x87\xB2\xF0\x9F\x87\xB0", // MK
            'ro' => "\xF0\x9F\x87\xB7\xF0\x9F\x87\xB4", // RO
            'bg' => "\xF0\x9F\x87\xA7\xF0\x9F\x87\xAC", // BG
            'gr' => "\xF0\x9F\x87\xAC\xF0\x9F\x87\xB7", // GR
            'hu' => "\xF0\x9F\x87\xAD\xF0\x9F\x87\xBA", // HU
            'ee' => "\xF0\x9F\x87\xAA\xF0\x9F\x87\xAA", // EE
            'lv' => "\xF0\x9F\x87\xB1\xF0\x9F\x87\xBB", // LV
            'lt' => "\xF0\x9F\x87\xB1\xF0\x9F\x87\xB9", // LT
            'al' => "\xF0\x9F\x87\xA6\xF0\x9F\x87\xB1", // AL
            'mt' => "\xF0\x9F\x87\xB2\xF0\x9F\x87\xB9", // MT
            'ie' => "\xF0\x9F\x87\xAE\xF0\x9F\x87\xAA", // IE
            'ir' => "\xF0\x9F\x87\xAE\xF0\x9F\x87\xB7", // IR
            'iq' => "\xF0\x9F\x87\xAE\xF0\x9F\x87\xB6", // IQ
            'az' => "\xF0\x9F\x87\xA6\xF0\x9F\x87\xBF", // AZ
            'am' => "\xF0\x9F\x87\xA6\xF0\x9F\x87\xB2", // AM
            'ge' => "\xF0\x9F\x87\xAC\xF0\x9F\x87\xAA", // GE
            'kz' => "\xF0\x9F\x87\xB0\xF0\x9F\x87\xBF", // KZ
            'uz' => "\xF0\x9F\x87\xBA\xF0\x9F\x87\xBF", // UZ
            'in' => "\xF0\x9F\x87\xAE\xF0\x9F\x87\xB3", // IN
            'bd' => "\xF0\x9F\x87\xA7\xF0\x9F\x87\xA9", // BD
            'pk' => "\xF0\x9F\x87\xB5\xF0\x9F\x87\xB0", // PK
            'np' => "\xF0\x9F\x87\xB3\xF0\x9F\x87\xB5", // NP
            'lk' => "\xF0\x9F\x87\xB1\xF0\x9F\x87\xB0", // LK
            'th' => "\xF0\x9F\x87\xB9\xF0\x9F\x87\xAD", // TH
            'vn' => "\xF0\x9F\x87\xBB\xF0\x9F\x87\xB3", // VN
            'id' => "\xF0\x9F\x87\xAE\xF0\x9F\x87\xA9", // ID
            'my' => "\xF0\x9F\x87\xB2\xF0\x9F\x87\xBE", // MY
            'mn' => "\xF0\x9F\x87\xB2\xF0\x9F\x87\xB3", // MN
            'ke' => "\xF0\x9F\x87\xB0\xF0\x9F\x87\xAA", // KE
            'so' => "\xF0\x9F\x87\xB8\xF0\x9F\x87\xB4", // SO
            'za' => "\xF0\x9F\x87\xBF\xF0\x9F\x87\xA6", // ZA
            // Extra country flags (used by variants)
            'ph' => "\xF0\x9F\x87\xB5\xF0\x9F\x87\xAD", // PH
            'at' => "\xF0\x9F\x87\xA6\xF0\x9F\x87\xB9", // AT
            'be' => "\xF0\x9F\x87\xA7\xF0\x9F\x87\xAA", // BE
            'ch' => "\xF0\x9F\x87\xA8\xF0\x9F\x87\xAD", // CH
            'ca' => "\xF0\x9F\x87\xA8\xF0\x9F\x87\xA6", // CA
            'au' => "\xF0\x9F\x87\xA6\xF0\x9F\x87\xBA", // AU
            'nz' => "\xF0\x9F\x87\xB3\xF0\x9F\x87\xBF", // NZ
        ];
    }

    /**
 * Render the post-save prompt when a language was just unchecked and
 * its content is still in the database. The admin picks per-language
 * what to do — move to Draft, permanently delete, or leave as-is.
 * Click = AJAX call to ajax_resolve_removed_language.
 */
    public function render_removed_language_prompt() {
        if (!current_user_can('manage_options')) return;
        $uid = get_current_user_id();
        $pending = get_transient('lf_removed_lang_pending_' . $uid);
        if (empty($pending) || !is_array($pending)) return;

        $nonce = wp_create_nonce('lf_resolve_removed_language');
        ?>
        <div class="notice notice-warning lf-removed-lang-notice" style="padding:16px 20px;border-left-color:#FF6B35;">
            <h3 style="margin:0 0 10px;font-size:15px;"><?php esc_html_e('Orphaned translations', 'langforge'); ?></h3>
            <p style="margin:0 0 12px;font-size:14px;color:#475569;">
                <?php esc_html_e('You removed languages from the active list, but the following translations are still published. What should happen to them?', 'langforge'); ?>
            </p>
            <table style="width:100%;max-width:720px;border-collapse:collapse;">
                <?php foreach ($pending as $code => $info): ?>
                <tr data-code="<?php echo esc_attr($code); ?>" style="border-bottom:1px solid #e5e7eb;">
                    <td style="padding:8px 6px;font-weight:600;min-width:140px;">
                        <?php echo esc_html($info['name']); ?> <code style="color:#94a3b8;"><?php echo esc_html($code); ?></code>
                    </td>
                    <td style="padding:8px 6px;color:#64748b;min-width:120px;">
                        <?php /* translators: %d: number of published posts in this language. */ printf(esc_html(_n('%d published post', '%d published posts', $info['count'], 'langforge')), esc_html($info['count'])); ?>
                    </td>
                    <td style="padding:8px 6px;">
                        <button type="button" class="button" data-action="leave"><?php esc_html_e('Leave published', 'langforge'); ?></button>
                        <button type="button" class="button" data-action="draft"><?php esc_html_e('Move to Draft', 'langforge'); ?></button>
                        <button type="button" class="button" data-action="delete" style="color:#b91c1c;"><?php esc_html_e('Delete permanently', 'langforge'); ?></button>
                        <span class="lf-rm-result" style="margin-left:8px;font-size:14px;"></span>
                    </td>
                </tr>
                <?php endforeach; ?>
            </table>
        </div>
        <script>
        jQuery(function($){
            $('.lf-removed-lang-notice').on('click', 'button[data-action]', function(){
                var $btn = $(this);
                var $row = $btn.closest('tr');
                var code = $row.data('code');
                var action = $btn.data('action');
                if (action === 'delete' && !confirm('<?php echo esc_js(__('Permanently delete all posts in this language? This cannot be undone.', 'langforge')); ?>')) return;
                $row.find('button').prop('disabled', true);
                $.post(ajaxurl, {
                    action: 'lf_resolve_removed_language',
                    nonce: '<?php echo esc_js($nonce); ?>',
                    code: code,
                    decision: action
                }, function(r){
                    if (r.success) {
                        $row.find('.lf-rm-result').css('color','#059669').text(r.data.message);
                        setTimeout(function(){ $row.fadeOut(250); }, 800);
                        if (r.data.all_resolved) {
                            setTimeout(function(){ $('.lf-removed-lang-notice').fadeOut(250); }, 900);
                        }
                    } else {
                        $row.find('button').prop('disabled', false);
                        $row.find('.lf-rm-result').css('color','#b91c1c').text(r.data.message || 'Error');
                    }
                });
            });
        });
        </script>
        <?php
    }

    /**
 * LF audit 2026-04-28 (R2-CP1): WPML × LangForge coexistence warning.
 *
 * When WPML core (SitePress) is active and configured alongside
 * LangForge, both plugins try to:
 * - rewrite language URLs (/en/foo, /ru/foo)
 * - own `<html lang>` / `<html dir>` attributes
 * - filter the same post meta and the post object itself
 * - register `?lang=` query var
 *
 * Frontend output ends up unpredictable (race depending on hook
 * priorities), and the admin sees double-language switchers, broken
 * URL routing, or missing translations. The supported flow is
 * "deactivate WPML, then migrate via Lang Forge → Migration → WPML".
 *
 * Notice is rendered on every wp-admin screen (not just LangForge
 * pages) because the conflict bites the site owner immediately on
 * the front of the site, regardless of which admin tab they happen
 * to open. Dismissible per WP convention, but with no persistent
 * "remind me never" — both plugins active is a fault state, not a
 * preference.
 */
    public function render_wpml_coexistence_notice() {
        if (!current_user_can('manage_options')) return;
        if (!class_exists('LF_Core') || !LF_Core::instance()->is_wpml_active()) return;

        $migration_url = admin_url('admin.php?page=lf-migration');
        ?>
        <div class="notice notice-warning is-dismissible lf-wpml-coexistence-notice">
            <p style="font-size:14px;line-height:1.55;margin:6px 0;">
                <strong style="color:#92400e;">⚠ <?php esc_html_e('WPML and Lang Forge are both active', 'langforge'); ?></strong><br>
                <?php esc_html_e('Two multilingual plugins running at the same time conflict on language routing and HTML meta — frontend output may be unpredictable.', 'langforge'); ?>
                <br>
                <?php
                printf(
                    /* translators: %s: URL to Lang Forge → Migration page */
                    esc_html__('To use Lang Forge, deactivate WPML on the Plugins page. To migrate WPML content into Lang Forge, run %s.', 'langforge'),
                    '<a href="' . esc_url($migration_url) . '"><strong>' . esc_html__('Lang Forge → Migration → WPML', 'langforge') . '</strong></a>'
                );
                ?>
            </p>
        </div>
        <?php
    }

    /**
 * LF audit 2026-04-28 (R2-CP1): inline, non-dismissible callout shown
 * at the top of Lang Forge → Languages when WPML is active alongside
 * LangForge. The settings page is the place where the site owner is
 * actively configuring language routing — this is where the conflict
 * matters most, so we list the concrete tables / behaviours that
 * collide rather than a one-line generic warning.
 */
    public function render_wpml_settings_conflict_callout() {
        if (!class_exists('LF_Core') || !LF_Core::instance()->is_wpml_active()) return;

        $migration_url = admin_url('admin.php?page=lf-migration');
        $plugins_url   = admin_url('plugins.php');
        ?>
        <div class="lf-wpml-conflict-callout" style="margin:16px 0;padding:14px 18px;border:1px solid #fde68a;border-left:4px solid #f59e0b;border-radius:8px;background:#fffbeb;">
            <p style="margin:0 0 8px;font-size:14px;font-weight:600;color:#92400e;">
                ⚠ <?php esc_html_e('WPML is active on this site — language routing will conflict', 'langforge'); ?>
            </p>
            <p style="margin:0 0 8px;font-size:13px;color:#475569;line-height:1.55;">
                <?php esc_html_e('Both plugins resolve the current language, register the same query var, and write language meta to the same posts. Concretely:', 'langforge'); ?>
            </p>
            <ul style="margin:0 0 10px 18px;padding:0;font-size:13px;color:#475569;line-height:1.6;">
                <li><?php esc_html_e('WPML\'s `wp_icl_translations` and Lang Forge\'s `wp_lf_translations` both answer translation queries — frontend output is unpredictable.', 'langforge'); ?></li>
                <li><?php esc_html_e('WPML\'s rewrite rules for /{lang}/ collide with Lang Forge\'s URL format.', 'langforge'); ?></li>
                <li><?php esc_html_e('`<html lang>` and `is_rtl()` may flip-flop between the two plugins on each request.', 'langforge'); ?></li>
            </ul>
            <p style="margin:0;font-size:13px;color:#475569;line-height:1.55;">
                <?php
                printf(
                    /* translators: 1: link to Plugins page, 2: link to Migration page */
                    esc_html__('To run only Lang Forge, deactivate WPML on the %1$s. To bring WPML data over, run %2$s — it imports active languages, translations, and string translations safely.', 'langforge'),
                    '<a href="' . esc_url($plugins_url) . '"><strong>' . esc_html__('Plugins page', 'langforge') . '</strong></a>',
                    '<a href="' . esc_url($migration_url) . '"><strong>' . esc_html__('Lang Forge → Migration → WPML', 'langforge') . '</strong></a>'
                );
                ?>
            </p>
        </div>
        <?php
    }

    public function ajax_resolve_removed_language() {
        check_ajax_referer('lf_resolve_removed_language', 'nonce');
        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Forbidden', 'langforge')], 403);
        }
        $uid      = get_current_user_id();
        $code     = sanitize_key(wp_unslash($_POST['code'] ?? ''));
        $decision = sanitize_key(wp_unslash($_POST['decision'] ?? ''));
        if (!$code || !in_array($decision, ['leave', 'draft', 'delete'], true)) {
            wp_send_json_error(['message' => 'Bad request']);
        }
        $pending = get_transient('lf_removed_lang_pending_' . $uid);
        if (!is_array($pending) || !isset($pending[$code])) {
            wp_send_json_error(['message' => 'Pending entry not found']);
        }

        global $wpdb;
        $post_ids = $wpdb->get_col($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- core table; admin read
            "SELECT p.ID FROM {$wpdb->posts} p
               JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
              WHERE pm.meta_key = '_lf_language' AND pm.meta_value = %s
                AND p.post_status IN ('publish','future','pending','private')",
            $code
        ));
        $applied = 0;
        if ($decision === 'draft') {
            foreach ($post_ids as $pid) {
                wp_update_post(['ID' => $pid, 'post_status' => 'draft']);
                $applied++;
            }
        } elseif ($decision === 'delete') {
            foreach ($post_ids as $pid) {
                if (wp_delete_post($pid, true)) $applied++;
            }
        }
        // 'leave' = no-op, just remove from pending

        unset($pending[$code]);
        if (empty($pending)) {
            delete_transient('lf_removed_lang_pending_' . $uid);
        } else {
            set_transient('lf_removed_lang_pending_' . $uid, $pending, HOUR_IN_SECONDS);
        }
        // New content state — clear the empty-languages cache so the
        // switcher re-evaluates on the next visit.
        if (method_exists(LF_Core::instance(), 'clear_empty_languages_cache')) {
            LF_Core::instance()->clear_empty_languages_cache();
        }

        $messages = [
            'leave'  => __('Left published. They stay in DB but not linked from the switcher.', 'langforge'),
            /* translators: %d: number of posts moved to Draft. */
            'draft'  => sprintf(__('Moved %d post(s) to Draft.', 'langforge'), $applied),
            /* translators: %d: number of posts deleted. */
            'delete' => sprintf(__('Deleted %d post(s).', 'langforge'), $applied),
        ];
        wp_send_json_success([
            'message'      => $messages[$decision],
            'all_resolved' => empty($pending),
        ]);
    }
}
