<?php
if (!defined('ABSPATH')) exit;

class SEOFORGE_Admin {
    private static $instance = null;
    public static function instance() { if (!self::$instance) self::$instance = new self(); return self::$instance; }

    private function __construct() {
        add_filter('plugin_action_links_' . SEOFORGE_BASENAME, [$this, 'settings_link']);
    }

    public function settings_link($links) {
        array_unshift($links, '<a href="' . admin_url('admin.php?page=seoforge-settings') . '">' . __('Settings', 'seoforge') . '</a>');
        return $links;
    }

    public static function sanitize_feature_settings(array $post, array $caps): array {
        $auto_links_allowed = !empty($caps['auto_links']);

        // the General → Features toggles were removed. The free modules
        // are hardcoded on; the Pro upsell flags (auto_meta / internal_links)
        // are preserved from existing settings since the form no longer submits
        // them (so a Pro user who enabled them keeps them).
        $existing = get_option('seoforge_settings', []);

        return [
            'settings' => [
                'auto_analyze' => true,
                'auto_meta' => !empty($caps['meta_generator']) && !empty($existing['auto_meta']),
                'schema_enabled' => true,
                'internal_links_enabled' => !empty($caps['internal_links']) && !empty($existing['internal_links_enabled']),
            ],
            'auto_links_enabled' => $auto_links_allowed && !empty($post['seoforge_auto_links_enabled']),
            'auto_links_max' => $auto_links_allowed
                ? max(1, min(10, (int) ($post['seoforge_auto_links_max'] ?? 3)))
                : 3,
        ];
    }

    public function render_settings() {
        wp_enqueue_media();

        // explicit take-over / remove actions for the physical robots.txt.
        // These share the save_settings nonce but do not require the "Save" submit
        // button to have been pressed (so they work via their own submit button).
        if (
            current_user_can('manage_options')
            && isset($_POST['seoforge_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['seoforge_nonce'])), 'seoforge_save_settings')
            && (!empty($_POST['seoforge_robots_takeover']) || !empty($_POST['seoforge_robots_remove']))
        ) {
            if (!empty($_POST['seoforge_robots_takeover'])) {
                // Prefer the textarea content that was just submitted.
                $custom = isset($_POST['seoforge_robots_txt'])
                    ? sanitize_textarea_field(wp_unslash($_POST['seoforge_robots_txt']))
                    : get_option('seoforge_robots_txt', '');
                update_option('seoforge_robots_txt', $custom);
                $written = SEOFORGE_Robots_Txt::instance()->write_if_possible($custom, true);
                echo '<div class="notice notice-' . ($written ? 'success' : 'error') . '"><p>' . esc_html($written
                    ? __('Physical robots.txt overwritten with your settings.', 'seoforge')
                    : __('Could not write robots.txt — check file permissions in the site root.', 'seoforge')) . '</p></div>';
            }
            if (!empty($_POST['seoforge_robots_remove'])) {
                $removed = SEOFORGE_Robots_Txt::instance()->remove_managed_file();
                echo '<div class="notice notice-' . ($removed ? 'success' : 'error') . '"><p>' . esc_html($removed
                    ? __('Physical robots.txt removed. The virtual filter now controls it.', 'seoforge')
                    : __('Nothing to remove, or file is not managed by SEO Forge.', 'seoforge')) . '</p></div>';
            }
        }

        if (isset($_POST['seoforge_save']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['seoforge_nonce'] ?? '')), 'seoforge_save_settings')) {
            $feature_settings = self::sanitize_feature_settings($_POST, [
                'meta_generator' => SEOFORGE_License::can('meta_generator'),
                'internal_links' => SEOFORGE_License::can('internal_links'),
                'auto_links' => SEOFORGE_License::can('auto_links'),
            ]);
            $settings = $feature_settings['settings'];
            update_option('seoforge_settings', $settings);

            // Save Default OG Image
            if (isset($_POST['seoforge_default_og_image'])) {
                update_option('seoforge_default_og_image', esc_url_raw(wp_unslash($_POST['seoforge_default_og_image'])));
            }

            // Save Auto-Links settings. Free/expired licenses force these off
            // so stale Pro settings cannot keep running after downgrade.
            update_option('seoforge_auto_links_enabled', $feature_settings['auto_links_enabled']);
            update_option('seoforge_auto_links_max', $feature_settings['auto_links_max']);

            // the "Suppress other SEO plugins" toggle was removed from
            // Settings → General; suppression is now always-on (hardcoded true in
            // SEOFORGE_Competitor_Coexistence). No option is written here, so the
            // removed checkbox can't silently save a `false`.

            // Google Search Console — OAuth now lives on forge-api
            // (client_id + client_secret stay in the Worker env); no
            // per-site credentials to save. Kept as a no-op marker so
            // the legacy form fields, if they linger in a stale
            // browser tab, don't throw a notice.

            // Save Title Templates
            if (isset($_POST['seoforge_title_separator'])) {
                update_option('seoforge_title_separator', sanitize_text_field(wp_unslash($_POST['seoforge_title_separator'])));
            }
            if (isset($_POST['seoforge_tpl']) && is_array($_POST['seoforge_tpl'])) {
                $templates = [];
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Unslashed here, each field sanitized per-field below (sanitize_text_field / sanitize_textarea_field).
                foreach (wp_unslash($_POST['seoforge_tpl']) as $pt => $tpl) {
                    $pt = sanitize_key($pt);
                    $templates[$pt] = [
                        'title'       => sanitize_text_field($tpl['title'] ?? ''),
                        'description' => sanitize_textarea_field($tpl['description'] ?? ''),
                    ];
                }
                update_option('seoforge_title_templates', $templates);
            }

            // Save Sitemap settings. `enabled_types` no longer
            // comes from this form — inclusion is driven by the per-
            // post-type "Show in search results" toggle saved via
            // `seoforge_pt_visible[...]` above. We still keep the key
            // on the stored option (empty array) so any custom code
            // reading the option doesn't break — but the sitemap
            // generator ignores it.
            $sitemap_settings = [
                'enabled_types' => [],
                'max_urls' => max(100, min(50000, absint( wp_unslash($_POST['sitemap_max_urls'] ?? 1000) ))),
            ];
            update_option('seoforge_sitemap_settings', $sitemap_settings);
            // Invalidate sitemap cache
            if (class_exists('SEOFORGE_Sitemap')) SEOFORGE_Sitemap::instance()->invalidate_cache();

            // Breadcrumbs settings
            if (isset($_POST['seoforge_breadcrumbs']) && is_array($_POST['seoforge_breadcrumbs'])) {
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Unslashed here, each field sanitized per-field below.
                $bc_in = wp_unslash($_POST['seoforge_breadcrumbs']);
                $bc = [
                    'enabled'      => !empty($bc_in['enabled']),
                    'separator'    => sanitize_text_field($bc_in['separator'] ?? '»'),
                    'home_text'    => sanitize_text_field($bc_in['home_text'] ?? 'Home'),
                    'show_current' => !empty($bc_in['show_current']),
                ];
                if ($bc['separator'] === '') $bc['separator'] = '»';
                if ($bc['home_text'] === '') $bc['home_text'] = 'Home';
                update_option('seoforge_breadcrumbs', $bc);
            }

            // Save Robots.txt (physical file mirrored automatically via
            // SEOFORGE_Robots_Txt::sync_physical_file hook on update_option).
            // when the on-disk file is unmanaged (server put it
            // there, not SEO Forge), the UI renders the textarea as
            // readonly. Browsers still submit readonly fields, so without
            // this guard we'd overwrite the stored option with the live
            // physical-file body each save — pinning the user to that
            // content even if they later delete the server-owned file.
            
            // (Azis feedback 2026-04-22): same guard applies to the
            // server-override (divergence) case — textarea is now readonly
            // there too, and browsers still submit. Recompute the HTTP-vs-
            // expected check so saves made while a CDN/proxy is intercepting
            // /robots.txt don't pin the stored option to whatever the
            // override currently returns.
            $sf_has_unmanaged_robots =
                SEOFORGE_Robots_Txt::physical_exists()
                && !SEOFORGE_Robots_Txt::physical_managed();

            $sf_has_robots_divergence = false;
            if (!$sf_has_unmanaged_robots) {
                SEOFORGE_Robots_Txt::instance()->ensure_managed_file_current(get_option('seoforge_robots_txt', ''));
            }
            if (!$sf_has_unmanaged_robots) {
                $sf_rx       = SEOFORGE_Robots_Txt::instance();
                $sf_saved    = get_option('seoforge_robots_txt', '');
                $sf_expected = $sf_rx->build_output($sf_saved);
                $sf_snap     = SEOFORGE_Robots_Txt::http_snapshot();
                $sf_body     = (string) ($sf_snap['body'] ?? '');
                if ($sf_body !== '') {
                    $sf_http_managed = strpos($sf_body, SEOFORGE_Robots_Txt::MANAGED_MARKER) !== false;
                    $sf_normalized   = trim(
                        ($sf_http_managed ? SEOFORGE_Robots_Txt::MANAGED_MARKER . "\n" : '')
                        . $sf_expected
                    );
                    if (trim($sf_body) !== $sf_normalized
                        && trim($sf_body) !== trim($sf_expected)) {
                        $sf_has_robots_divergence = true;
                    }
                }
            }

            if (isset($_POST['seoforge_robots_txt']) && !$sf_has_unmanaged_robots && !$sf_has_robots_divergence) {
                $robots_body = sanitize_textarea_field(wp_unslash($_POST['seoforge_robots_txt']));
                update_option('seoforge_robots_txt', $robots_body);

                // (retest 2026-04-16): ALWAYS try to mirror to the
                // physical file on save — force_takeover=true so existing
                // un-managed files are overwritten, and write_if_possible
                // internally falls through unlink+write + WP_Filesystem if
                // plain file_put_contents fails. Hostinger ships a default
                // /robots.txt owned by the web user with permissions that
                // block direct writes; the fallbacks handle that.
                $rx_auto    = SEOFORGE_Robots_Txt::instance();
                $had_phys   = SEOFORGE_Robots_Txt::physical_exists();
                $write_ok   = $rx_auto->write_if_possible($robots_body, true);

                // Purge common host caches so the new content is served
                // immediately. Safe no-ops when the cache plugins aren't
                // installed.
                if (function_exists('wp_cache_flush')) wp_cache_flush();
                if (class_exists('\\LiteSpeed\\Purge')) {
                    do_action('litespeed_purge', 'FO'); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional ACF/WPML/core compatibility
                    do_action('litespeed_purge_url', home_url('/robots.txt')); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals -- intentional ACF/WPML/core compatibility
                }
                if (function_exists('rocket_clean_domain')) rocket_clean_domain();

                // Surface the result to the admin so they don't fly blind.
                if ($had_phys && ! $write_ok) {
                    add_settings_error(
                        'seoforge_settings',
                        'seoforge_robots_write_failed',
                        __('Robots.txt settings saved, but the physical /robots.txt file is not writable by PHP. Delete it via FTP/SSH — SEO Forge will then serve the virtual robots.txt built from your settings.', 'seoforge'),
                        'warning'
                    );
                }
            }

            // Save Archive noindex
            $archive_noindex = [];
            if (!empty($_POST['archive_noindex']) && is_array($_POST['archive_noindex'])) {
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Keys sanitized via sanitize_key below; values are hardcoded booleans.
                foreach ($_POST['archive_noindex'] as $pt => $val) {
                    $archive_noindex[sanitize_key($pt)] = ['noindex' => true];
                }
            }
            update_option('seoforge_archive_robots', $archive_noindex);

            // Save Post Type search visibility (noindex for all posts of a type)
            $pt_robots = [];
            $all_public_types = get_post_types(['public' => true], 'names');
            unset($all_public_types['attachment']);
            foreach ($all_public_types as $pt_name) {
                // If checkbox NOT checked → noindex
                if (empty($_POST['seoforge_pt_visible'][$pt_name])) {
                    $pt_robots[$pt_name] = ['noindex' => true];
                }
            }
            update_option('seoforge_posttype_robots', $pt_robots);

            // noindex, follow on paginated archives
            update_option('seoforge_noindex_paginated', !empty($_POST['seoforge_noindex_paginated']));

            echo '<div class="notice notice-success"><p>' . esc_html__('Settings saved.', 'seoforge') . '</p></div>';
        }

        $s = get_option('seoforge_settings', []);
        $sf_can_auto_meta = SEOFORGE_License::can('meta_generator');
        $sf_can_internal_links = SEOFORGE_License::can('internal_links');
        $sf_can_auto_links = SEOFORGE_License::can('auto_links');
        $sf_can_gsc = SEOFORGE_License::can('gsc');
        // Settings now uses a vertical sidebar layout (Yoast-style).
        // Tabs group the 10 cards by user intent. All cards stay in DOM so the
        // single <form> still submits every field — JS only toggles visibility.
        $sf_tabs = [
            'general'    => ['label' => __('General',          'seoforge'), 'icon' => 'admin-settings',    'desc' => __('License, AI access and which SEO Forge features are turned on.', 'seoforge')],
            'search'     => ['label' => __('Search Appearance','seoforge'), 'icon' => 'visibility',        'desc' => __('How your site looks in Google: title templates, sitemap, robots, archive visibility.', 'seoforge')],
            'social'     => ['label' => __('Social',           'seoforge'), 'icon' => 'share',             'desc' => __('Default Open Graph image and how posts look when shared.', 'seoforge')],
            'automation' => ['label' => __('Automation',       'seoforge'), 'icon' => 'admin-links',       'desc' => __('Auto-link related posts on publish, run bulk analysis on the whole site.', 'seoforge')],
            'integrations'=>['label' => __('Integrations',     'seoforge'), 'icon' => 'admin-plugins',     'desc' => __('Connect external services like Google Search Console.', 'seoforge')],
        ];
        ?>
        <div class="wrap sf-settings-wrap sf-admin">
            <?php SEOFORGE_Admin_Tabs::render('seoforge-settings'); ?>
            <h1><?php esc_html_e('SEO Forge Settings', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'settings-overview', __('Settings overview', 'seoforge'), __('What each tab controls and which options matter first.', 'seoforge')); ?></h1>

            <form method="post" class="sf-settings-form">
                <?php wp_nonce_field('seoforge_save_settings', 'seoforge_nonce'); ?>

                <div class="sf-settings-layout">
                    <!-- Sidebar navigation -->
                    <nav class="sf-settings-nav" role="tablist" aria-label="<?php esc_attr_e('Settings sections', 'seoforge'); ?>">
                        <!-- Mobile dropdown (visible <960px) -->
                        <select class="sf-settings-nav__select" aria-label="<?php esc_attr_e('Jump to section', 'seoforge'); ?>">
                            <?php foreach ($sf_tabs as $key => $t): ?>
                                <option value="<?php echo esc_attr($key); ?>"><?php echo esc_html($t['label']); ?></option>
                            <?php endforeach; ?>
                        </select>
                        <!-- Sidebar tabs (visible ≥960px) -->
                        <ul class="sf-settings-nav__list">
                        <?php foreach ($sf_tabs as $key => $t): ?>
                            <li>
                                <a href="#sf-tab-<?php echo esc_attr($key); ?>" role="tab" data-sf-tab="<?php echo esc_attr($key); ?>" class="sf-settings-nav__item<?php echo $key === 'general' ? ' is-active' : ''; ?>" aria-controls="sf-tab-<?php echo esc_attr($key); ?>" aria-selected="<?php echo $key === 'general' ? 'true' : 'false'; ?>">
                                    <span class="dashicons dashicons-<?php echo esc_attr($t['icon']); ?>" aria-hidden="true"></span>
                                    <span class="sf-settings-nav__label"><?php echo esc_html($t['label']); ?></span>
                                </a>
                            </li>
                        <?php endforeach; ?>
                        </ul>
                    </nav>

                    <!-- Content panels -->
                    <div class="sf-settings-content">
                    <?php foreach ($sf_tabs as $key => $t): ?>
                        <header class="sf-settings-panel-head<?php echo $key === 'general' ? ' is-active' : ''; ?>" data-sf-panel-head="<?php echo esc_attr($key); ?>">
                            <h2><?php echo esc_html($t['label']); ?></h2>
                            <p><?php echo esc_html($t['desc']); ?></p>
                        </header>
                    <?php endforeach; ?>

                <div class="sf-settings-cards">

                <!-- AI Card -->
                <div class="sf-card is-active" data-sf-tab="general">
                    <div class="sf-card-head"><span class="dashicons dashicons-superhero-alt"></span><h2><?php esc_html_e('AI-Powered SEO', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'using-ai-to-write-titles-and-descriptions-pro', __('AI-powered SEO', 'seoforge'), __('What the AI can write for you and what it costs in credits.', 'seoforge')); ?></h2></div>
                    <div class="sf-card-body">
                        <?php if (!SEOFORGE_License::instance()->is_free()): ?>
                            <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 SEO features are active. Powered by Forge AI — meta generation, content analysis, internal links, and more.', 'seoforge'); ?>
                            </div>
                        <?php else: ?>
                            <div style="font-size:14px;color:#64748b;">
                                <?php esc_html_e('AI-powered SEO features require a Pro license. All AI processing is handled securely through the Forge API — no external API keys needed.', 'seoforge'); ?>
                                <a href="<?php echo esc_url(SEOFORGE_License::instance()->upgrade_url()); ?>" target="_blank" style="color:#FF6B35;font-weight:600;margin-left:4px;"><?php esc_html_e('Upgrade to Pro', 'seoforge'); ?></a>
                            </div>
                        <?php endif; ?>
                    </div>
                </div>

                <?php
                // the "Features" card was removed to declutter Settings and
                // commit to "works out of the box". The free modules it toggled
                // (Auto-analyze on save, Schema.org JSON-LD, Suppress other SEO
                // plugins) are now always-on at the code level — auto_analyze and
                // schema never read a setting at runtime anyway, and
                // seoforge_suppress_competitors is hardcoded true in
                // SEOFORGE_Competitor_Coexistence. The Pro upsell toggles
                // (Auto-generate meta, Internal link suggestions) live in the post
                // metabox with their paywall, so they don't belong here either.
                ?>

                <!-- Auto Internal Links Card -->
                <div class="sf-card" data-sf-tab="automation">
                    <div class="sf-card-head"><span class="dashicons dashicons-admin-links"></span><h2><?php esc_html_e('Auto Internal Links on Publish', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'auto-internal-links-pro', __('Auto internal linking', 'seoforge'), __('What it inserts, how many links per post, how to tune.', 'seoforge')); ?></h2></div>
                    <div class="sf-card-body">
                        <p style="margin:0 0 14px;font-size:14px;color:#64748b;"><?php esc_html_e('When a post is published for the first time, automatically insert internal links to related posts and add backlinks from them.', 'seoforge'); ?></p>
                        <?php
                        $al_enabled  = get_option('seoforge_auto_links_enabled', false);
                        $al_max      = (int) get_option('seoforge_auto_links_max', 3);
                        ?>
                        <div class="sf-toggles">
                            <label class="sf-toggle">
                                <?php if ($sf_can_auto_links): ?>
                                <input type="checkbox" name="seoforge_auto_links_enabled" value="1" <?php checked($al_enabled); ?>>
                                <?php else: ?>
                                <span class="dashicons dashicons-lock" aria-hidden="true" style="color:#FF6B35;margin-top:2px;"></span>
                                <?php endif; ?>
                                <span class="sf-toggle-text">
                                    <strong><?php esc_html_e('Auto internal linking on publish', 'seoforge'); ?><?php if (!$sf_can_auto_links): ?> <a href="<?php echo esc_url(SEOFORGE_License::instance()->upgrade_url()); ?>" target="_blank" style="color:#FF6B35;"><?php esc_html_e('Upgrade to Pro', 'seoforge'); ?></a><?php endif; ?></strong>
                                    <span><?php esc_html_e('Automatically insert contextual internal links when a post is first published', 'seoforge'); ?></span>
                                </span>
                            </label>
                        </div>
                        <?php if ($sf_can_auto_links): ?>
                        <div style="margin-top:14px;display:flex;align-items:center;gap:12px;">
                            <label class="sf-label" style="margin-bottom:0;"><?php esc_html_e('Max links per post', 'seoforge'); ?></label>
                            <input type="number" name="seoforge_auto_links_max" value="<?php echo esc_attr($al_max); ?>" min="1" max="10" step="1" class="sf-input" style="width:80px;">
                            <span style="font-size:14px;color:#94a3b8;"><?php esc_html_e('(1–10, default 3)', 'seoforge'); ?></span>
                        </div>
                        <?php endif; ?>
                    </div>
                </div>

                <!-- Bulk Analysis Card -->
                <div class="sf-card" data-sf-tab="automation">
                    <div class="sf-card-head"><span class="dashicons dashicons-database"></span><h2><?php esc_html_e('Bulk Analysis', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'fixing-seo-issues-with-ai-pro', __('Running a site-wide analysis', 'seoforge'), __('Analyzes every post in one pass. Takes a minute per 100 posts.', 'seoforge')); ?></h2></div>
                    <div class="sf-card-body">
                        <p style="margin:0 0 12px;font-size:14px;color:#64748b;"><?php esc_html_e('Analyze all published posts and pages at once.', 'seoforge'); ?></p>
                        <button type="button" id="seoforge-bulk-analyze" style="background:#FF6B35;color:#fff;border:none;border-radius:8px;padding:9px 20px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.15s;">
                            <span class="dashicons dashicons-chart-bar" style="font-size:14px;width:14px;height:14px;margin-top:2px;margin-right:4px;"></span>
                            <?php esc_html_e('Analyze All Posts', 'seoforge'); ?>
                        </button>
                        <div id="seoforge-bulk-progress" style="display:none;margin-top:12px;">
                            <div style="height:6px;background:#e2e8f0;border-radius:3px;max-width:400px;overflow:hidden;">
                                <div id="seoforge-bulk-bar" style="height:100%;width:0%;background:#FF6B35;border-radius:3px;transition:width 0.3s;"></div>
                            </div>
                            <p id="seoforge-bulk-status" style="font-size:14px;color:#64748b;margin-top:6px;"></p>
                        </div>
                    </div>
                </div>

                <!-- Default Social Image Card -->
                <div class="sf-card" data-sf-tab="social">
                    <div class="sf-card-head"><span class="dashicons dashicons-format-image"></span><h2><?php esc_html_e('Default Social Image', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'serp-preview-how-your-page-looks-in-google', __('Social + Open Graph images', 'seoforge'), __('Fallback image when a post has no featured image set.', 'seoforge')); ?></h2></div>
                    <div class="sf-card-body">
                        <p style="margin:0 0 12px;font-size:14px;color:#64748b;"><?php esc_html_e('Fallback image for Open Graph and Twitter cards when a post has no featured image.', 'seoforge'); ?></p>
                        <?php $default_og_image = get_option('seoforge_default_og_image', ''); ?>
                        <div style="display:flex;align-items:flex-start;gap:16px;">
                            <div id="sf-og-image-preview" style="width:200px;height:105px;border:2px dashed #e2e8f0;border-radius:8px;overflow:hidden;display:flex;align-items:center;justify-content:center;background:#f8fafc;flex-shrink:0;">
                                <?php if ($default_og_image): ?>
                                    <img src="<?php echo esc_url($default_og_image); ?>" style="width:100%;height:100%;object-fit:cover;">
                                <?php else: ?>
                                    <span style="color:#94a3b8;font-size:14px;"><?php esc_html_e('No image selected', 'seoforge'); ?></span>
                                <?php endif; ?>
                            </div>
                            <div>
                                <input type="hidden" name="seoforge_default_og_image" id="sf-og-image-url" value="<?php echo esc_attr($default_og_image); ?>">
                                <button type="button" id="sf-og-image-upload" style="background:#FF6B35;color:#fff;border:none;border-radius:8px;padding:9px 20px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.15s;">
                                    <span class="dashicons dashicons-upload" style="font-size:14px;width:14px;height:14px;margin-top:2px;margin-right:4px;"></span>
                                    <?php esc_html_e('Select Image', 'seoforge'); ?>
                                </button>
                                <?php if ($default_og_image): ?>
                                <button type="button" id="sf-og-image-remove" style="background:none;border:1px solid #e2e8f0;border-radius:8px;padding:9px 16px;font-size:14px;color:#64748b;cursor:pointer;margin-left:6px;">
                                    <?php esc_html_e('Remove', 'seoforge'); ?>
                                </button>
                                <?php endif; ?>
                                <p style="font-size:14px;color:#94a3b8;margin:8px 0 0;"><?php esc_html_e('Recommended size: 1200 x 630 pixels. Used for og:image and twitter:image when no featured image is set.', 'seoforge'); ?></p>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Title Templates Card -->
                <div class="sf-card" data-sf-tab="search">
                    <div class="sf-card-head"><span class="dashicons dashicons-editor-textcolor"></span><h2><?php esc_html_e('Title Templates', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'title-and-description-templates', __('Title and description templates', 'seoforge'), __('Placeholders like %%title%% and %%sep%% explained with examples.', 'seoforge')); ?></h2></div>
                    <div class="sf-card-body">
                        <p style="margin:0 0 16px;font-size:14px;color:#64748b;"><?php esc_html_e('Define default Title and Description templates per post type. Templates are used when no manual override is set.', 'seoforge'); ?></p>

                        <?php
                        $tpl_instance = SEOFORGE_Meta_Templates::instance();
                        $saved_templates = get_option('seoforge_title_templates', []);
                        $defaults = $tpl_instance->get_default_templates();
                        $variables = $tpl_instance->get_available_variables();
                        $current_sep = $tpl_instance->get_separator();
                        $separators = ['|' => '|', '-' => '-', '—' => '—', '·' => '·', '»' => '»', '/' => '/'];
                        $post_types = get_post_types(['public' => true], 'objects');
                        ?>

                        <!-- Separator -->
                        <div style="margin-bottom:20px;">
                            <label class="sf-label"><?php esc_html_e('Separator', 'seoforge'); ?></label>
                            <select name="seoforge_title_separator" class="sf-select" style="width:120px;" id="sf-tpl-separator">
                                <?php foreach ($separators as $val => $label): ?>
                                <option value="<?php echo esc_attr($val); ?>" <?php selected($current_sep, $val); ?>><?php echo esc_html($label); ?></option>
                                <?php endforeach; ?>
                            </select>
                        </div>

                        <?php
                        // global "Insert Variable" block removed. Each template
                        // field now gets its own inline {…} Add Variable trigger + popover
                        // (wired up in the template-block loop below via class-core JS).
                        // Variables data is still exposed for that JS.
                        ?>
                        <script>
                        window.SF_TEMPLATE_VARIABLES = <?php echo wp_json_encode(array_map(function($var, $desc){ return ['var' => $var, 'desc' => $desc]; }, array_keys($variables), array_values($variables))); ?>;
                        </script>

                        <!-- Per post type templates -->
                        <?php
                        foreach ($post_types as $pt):
                            if ($pt->name === 'attachment') continue;
                            $tpl_title = $saved_templates[$pt->name]['title'] ?? ($defaults[$pt->name]['title'] ?? '{{title}} {{separator}} {{siteName}}');
                            $tpl_desc = $saved_templates[$pt->name]['description'] ?? ($defaults[$pt->name]['description'] ?? '{{excerpt}}');

                            // Use site info as a generic placeholder preview instead of fetching real posts
                            $preview_title = get_bloginfo('name') . ' - ' . get_bloginfo('description');
                            $preview_desc  = get_bloginfo('description');
                        ?>
                        <?php
                            $pt_robots = get_option('seoforge_posttype_robots', []);
                            $pt_visible = empty($pt_robots[$pt->name]['noindex']);
                        ?>
                        <div class="sf-tpl-block" style="margin-bottom:20px;padding:16px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;">
                            <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
                                <h3 style="margin:0;font-size:14px;font-weight:600;color:#1e293b;"><?php echo esc_html($pt->labels->singular_name); ?></h3>
                                <label style="display:inline-flex;align-items:center;gap:6px;font-size:14px;color:<?php echo $pt_visible ? '#10b981' : '#dc2626'; ?>;">
                                    <input type="checkbox" name="seoforge_pt_visible[<?php echo esc_attr($pt->name); ?>]" value="1" <?php checked($pt_visible); ?>>
                                    <?php esc_html_e('Show in search results', 'seoforge'); ?>
                                </label>
                            </div>
                            <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;">
                                <div>
                                    <label class="sf-label"><?php esc_html_e('Title Template', 'seoforge'); ?></label>
                                    <div class="sf-tpl-field-wrap" style="position:relative;">
                                        <input type="text" name="seoforge_tpl[<?php echo esc_attr($pt->name); ?>][title]" value="<?php echo esc_attr($tpl_title); ?>" class="sf-input sf-tpl-field" data-pt="<?php echo esc_attr($pt->name); ?>" data-field="title" style="padding-right:34px;">
                                        <button type="button" class="sf-var-trigger" title="<?php esc_attr_e('Insert variable (or type { in the field)', 'seoforge'); ?>" aria-label="<?php esc_attr_e('Insert variable', 'seoforge'); ?>">
                                            <span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>
                                        </button>
                                    </div>
                                </div>
                                <div>
                                    <label class="sf-label"><?php esc_html_e('Description Template', 'seoforge'); ?></label>
                                    <div class="sf-tpl-field-wrap" style="position:relative;">
                                        <textarea name="seoforge_tpl[<?php echo esc_attr($pt->name); ?>][description]" class="sf-input sf-tpl-field" data-pt="<?php echo esc_attr($pt->name); ?>" data-field="description" rows="2" style="resize:vertical;padding-right:34px;"><?php echo esc_textarea($tpl_desc); ?></textarea>
                                        <button type="button" class="sf-var-trigger" title="<?php esc_attr_e('Insert variable (or type { in the field)', 'seoforge'); ?>" aria-label="<?php esc_attr_e('Insert variable', 'seoforge'); ?>">
                                            <span class="dashicons dashicons-plus-alt2" aria-hidden="true"></span>
                                        </button>
                                    </div>
                                </div>
                            </div>
                            <?php if ($preview_title || $preview_desc): ?>
                            <div class="sf-tpl-preview" data-pt="<?php echo esc_attr($pt->name); ?>" style="margin-top:12px;padding:10px 14px;background:#fff;border:1px solid #e2e8f0;border-radius:8px;">
                                <div style="font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;color:#94a3b8;margin-bottom:6px;"><?php esc_html_e('Live Preview', 'seoforge'); ?></div>
                                <div class="sf-preview-title" style="font-size:16px;color:#1a0dab;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><?php echo esc_html($preview_title); ?></div>
                                <div class="sf-preview-desc" style="font-size:14px;color:#545454;margin-top:2px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;"><?php echo esc_html($preview_desc); ?></div>
                            </div>
                            <?php endif; ?>
                        </div>
                        <?php endforeach; ?>

                        <p style="font-size:14px;color:#64748b;margin:0;"><strong><?php esc_html_e('Cascade:', 'seoforge'); ?></strong> <?php esc_html_e('Post-level override > Taxonomy template > Global template > Default', 'seoforge'); ?></p>
                    </div>
                </div>

                <!-- GSC Connection Card — OAuth goes through api.avakode.com now,
 so there are no client_id / client_secret fields to paste. A
 single Connect click covers every Google feature (rankings,
 Indexing, GA4 when we ship it). -->
                <div class="sf-card" data-sf-tab="integrations">
                    <div class="sf-card-head"><span class="dashicons dashicons-chart-line"></span><h2><?php esc_html_e('Google Search Console', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'google-search-console-integration-pro', __('Connecting Google Search Console', 'seoforge'), __('One Connect click authorizes the Search Console, GA4, and Indexing features together.', 'seoforge')); ?></h2></div>
                    <div class="sf-card-body">
                        <?php
                        // SEOFORGE_GSC lives in seoforge-pro (post-split). On
                        // Free-only sites we still render the card so the user
                        // can see the upgrade story, but the "Connect" button
                        // links to the upgrade page instead of OAuth.
                        $gsc_class_available = $sf_can_gsc && class_exists('SEOFORGE_GSC');
                        $gsc_connected = $gsc_class_available ? SEOFORGE_GSC::instance()->is_connected() : false;
                        $gsc_email = $gsc_class_available ? SEOFORGE_GSC::get_connected_email() : '';
                        // the active Search Console property the plugin
                        // pulls data from. Stored by detect_site_property /
                        // the "Change property" picker; we surface it here so
                        // the user always sees WHICH domain is active.
                        $gsc_property = (string) get_option('seoforge_gsc_property', '');
                        // Part B: open the property-selection wizard when
                        // OAuth just completed without a property (handle_post_connect
                        // adds ?sf_gsc_setup=1) OR whenever we're connected but no
                        // property is set yet. A re-auth that already has a property
                        // skips straight to the status line.
                        $gsc_needs_property = $gsc_connected && $gsc_property === '';
                        $gsc_run_wizard = $gsc_needs_property
                            || ($gsc_connected && !empty($_GET['sf_gsc_setup']));
                        ?>
                        <?php if ($gsc_connected): ?>
                            <p style="color:#059669;font-weight:600;margin:0 0 8px;">&#10003;
                                <?php if ($gsc_email !== ''): ?>
                                    <?php printf(
                                        /* translators: %s = Google account email used to connect */
                                        esc_html__('Connected as %s', 'seoforge'),
                                        '<span style="color:#0f172a;">' . esc_html($gsc_email) . '</span>'
                                    ); ?>
                                <?php else: ?>
                                    <?php esc_html_e('Connected', 'seoforge'); ?>
                                <?php endif; ?>
                                <?php // Part A: "Connected as … | Active Property: example.com". ?>
                                <?php if ($gsc_property !== ''): ?>
                                    <span style="color:#94a3b8;font-weight:400;margin:0 4px;">|</span>
                                    <?php esc_html_e('Active Property:', 'seoforge'); ?>
                                    <span id="sf-gsc-current-prop" style="color:#0f172a;"><?php echo esc_html($gsc_property); ?></span>
                                <?php else: ?>
                                    <span style="color:#94a3b8;font-weight:400;margin:0 4px;">|</span>
                                    <span id="sf-gsc-current-prop" style="color:#b45309;"><?php esc_html_e('No property selected yet', 'seoforge'); ?></span>
                                <?php endif; ?>
                            </p>
                            <?php if ($gsc_needs_property): ?>
                                <?php // Part B: lightweight post-OAuth wizard — highlighted prompt above the (auto-opened) property picker. ?>
                                <p style="font-size:14px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-left:3px solid #FF6B35;border-radius:6px;padding:10px 14px;margin:0 0 12px;">
                                    <strong><?php esc_html_e('Select your property', 'seoforge'); ?></strong> &mdash;
                                    <?php esc_html_e('Pick which Search Console property this site should pull rankings and decay data from. Rank Tracking stays paused until one is chosen.', 'seoforge'); ?>
                                </p>
                            <?php else: ?>
                                <p style="font-size:14px;color:#64748b;margin:0 0 12px;"><?php esc_html_e('Rank Tracking and Content Decay detection are active. Data syncs daily.', 'seoforge'); ?></p>
                            <?php endif; ?>

                            <!-- domain picker — load verified properties list + switch. -->
                            <!-- Both action buttons share a single flex row so they
 read as one action group instead of stacking with
 awkward whitespace. -->
                            <div id="sf-gsc-actions" style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0 0 10px;">
                                <button type="button" class="button" id="sf-gsc-change-prop">
                                    <span class="dashicons dashicons-admin-site-alt3" style="font-size:16px;width:16px;height:16px;line-height:1;display:inline-flex;align-items:center;margin-right:4px;"></span>
                                    <?php esc_html_e('Change property', 'seoforge'); ?>
                                </button>
                                <button type="button" class="button" id="sf-gsc-disconnect" style="color:#b91c1c !important;border-color:#fecaca !important;background:#fff !important;">
                                    <span class="dashicons dashicons-no-alt" style="font-size:16px;width:16px;height:16px;line-height:1;display:inline-flex;align-items:center;margin-right:4px;color:#b91c1c !important;"></span>
                                    <?php esc_html_e('Disconnect Google', 'seoforge'); ?>
                                </button>
                            </div>
                            <div id="sf-gsc-picker">
                                <?php // Part B: same picker the "Change property" button drives — auto-opened (not hidden) when the post-OAuth wizard needs a property. ?>
                                <div id="sf-gsc-picker-body"<?php echo $gsc_run_wizard ? '' : ' hidden'; ?> style="margin-top:6px;padding:12px 14px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;">
                                    <label for="sf-gsc-picker-select" style="display:block;font-size:13px;color:#64748b;margin-bottom:6px;">
                                        <?php if ($gsc_needs_property): ?>
                                            <?php esc_html_e('Step 1 — choose the Search Console property for this site', 'seoforge'); ?>
                                        <?php else: ?>
                                            <?php esc_html_e('Pick the property this site maps to', 'seoforge'); ?>
                                        <?php endif; ?>
                                    </label>
                                    <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
                                        <select id="sf-gsc-picker-select" style="min-width:260px;max-width:100%;flex:1;"></select>
                                        <button type="button" class="button button-primary" id="sf-gsc-picker-save"<?php echo $gsc_needs_property ? ' style="background:#FF6B35;border-color:#FF6B35;"' : ''; ?>><?php echo $gsc_needs_property ? esc_html__('Step 2 — Confirm', 'seoforge') : esc_html__('Save', 'seoforge'); ?></button>
                                    </div>
                                    <span id="sf-gsc-picker-status" style="display:block;margin-top:6px;font-size:13px;color:#64748b;min-height:18px;"></span>
                                </div>
                            </div>
                            <script>
                            (function(){
                                var btn     = document.getElementById('sf-gsc-change-prop');
                                var wrap    = document.getElementById('sf-gsc-picker-body');
                                var select  = document.getElementById('sf-gsc-picker-select');
                                var save    = document.getElementById('sf-gsc-picker-save');
                                var status  = document.getElementById('sf-gsc-picker-status');
                                var current = document.getElementById('sf-gsc-current-prop');
                                if (!btn || !wrap || !select || !save) return;
 // Part B: when no property is set yet, the wizard
                                // owns the picker — it's pre-opened server-side and the
                                // sites list auto-loads below.
                                var runWizard = <?php echo $gsc_run_wizard ? 'true' : 'false'; ?>;
                                var loaded = false;
                                function clearSelect(){ while (select.firstChild) select.removeChild(select.firstChild); }
                                function loadSites(onDone){
                                    if (loaded) { if (onDone) onDone(); return; }
                                    status.textContent = 'Loading…';
                                    var fd = new FormData();
                                    fd.append('action', 'seoforge_gsc_sites');
                                    fd.append('nonce', <?php echo wp_json_encode(wp_create_nonce('seoforge_nonce')); ?>);
                                    fetch(ajaxurl, { method:'POST', body: fd, credentials:'same-origin' })
                                        .then(function(r){ return r.json(); })
                                        .then(function(r){
                                            status.textContent = '';
                                            if (!r.success || !r.data || !r.data.sites) { status.textContent = 'Could not list properties.'; return; }
                                            clearSelect();
                                            r.data.sites.forEach(function(s){
                                                var opt = document.createElement('option');
                                                opt.value = s.siteUrl || '';
                                                opt.textContent = s.siteUrl || '(unknown)';
                                                if ((r.data.current || '') === opt.value) opt.selected = true;
                                                select.appendChild(opt);
                                            });
                                            loaded = true;
                                            if (onDone) onDone();
                                        })
                                        .catch(function(){ status.textContent = 'Network error'; });
                                }
                                btn.addEventListener('click', function(){
                                    wrap.hidden = !wrap.hidden;
                                    if (wrap.hidden) return;
                                    loadSites();
                                });
                                save.addEventListener('click', function(){
                                    if (!select.value) return;
                                    save.disabled = true;
                                    status.textContent = 'Saving…';
                                    var fd = new FormData();
                                    fd.append('action', 'seoforge_gsc_save_property');
                                    fd.append('nonce', <?php echo wp_json_encode(wp_create_nonce('seoforge_nonce')); ?>);
                                    fd.append('site_url', select.value);
                                    fetch(ajaxurl, { method:'POST', body: fd, credentials:'same-origin' })
                                        .then(function(r){ return r.json(); })
                                        .then(function(r){
                                            save.disabled = false;
                                            if (!r.success) { status.textContent = 'Save failed.'; return; }
                                            status.textContent = 'Saved. Reloading…';
                                            if (current) current.textContent = select.value;
                                            // Drop ?sf_gsc_setup so a reload doesn't re-trigger the wizard.
                                            var clean = window.location.pathname + window.location.search.replace(/[?&]sf_gsc_setup=1/, '').replace(/^&/, '?');
                                            setTimeout(function(){ window.location.replace(clean + '#sf-tab-integrations'); }, 500);
                                        })
                                        .catch(function(){ save.disabled = false; status.textContent = 'Network error'; });
                                });
 // Part B: auto-run the wizard — load the sites list
                                // and focus the dropdown so the user just picks + confirms.
                                if (runWizard) {
                                    loadSites(function(){ try { select.focus(); } catch(e){} });
                                }
                            })();
                            </script>
                        <?php else: ?>
                            <p style="font-size:14px;color:#64748b;margin:0 0 12px;"><?php esc_html_e('One Connect click authorizes Search Console, GA4, and Indexing at once. The OAuth flow runs on api.avakode.com — your site never stores Google tokens.', 'seoforge'); ?></p>
                            <?php
                            // Same-tab navigation: pass the current Settings page so the
                            // user lands back here after the Worker callback 302s them
                            // home with ?gsc_connected=1.
                            // carry `tab=integrations` in the return_url so the
                            // post-OAuth landing targets the Integrations tab, not
                            // General. It's a query param (survives the Worker's
                            // ?gsc_connected=1 append intact); handle_post_connect
                            // then redirects to the #sf-tab-integrations deep link.
                            $auth_url = $gsc_class_available
                                ? SEOFORGE_GSC::instance()->get_auth_url(
                                    admin_url('admin.php?page=seoforge-settings&tab=integrations')
                                )
                                : '';
                            if (!$sf_can_gsc):
                            ?>
                            <p style="font-size:14px;color:#64748b;margin:0 0 12px;"><?php esc_html_e('Google Search Console integration requires a SEO Forge Pro license.', 'seoforge'); ?></p>
                            <a href="<?php echo esc_url(SEOFORGE_License::instance()->upgrade_url()); ?>" target="_blank" style="display:inline-flex;align-items:center;gap:8px;background:#FF6B35;color:#fff;text-decoration:none;padding:10px 22px;border-radius:8px;font-size:14px;font-weight:600;"><?php esc_html_e('Upgrade to Pro', 'seoforge'); ?></a>
                            <?php
                            elseif ($auth_url):
                            ?>
                            <a href="<?php echo esc_url($auth_url); ?>" id="sf-google-connect" style="display:inline-flex;align-items:center;gap:8px;background:#FF6B35;color:#fff;text-decoration:none;padding:10px 22px;border-radius:8px;font-size:14px;font-weight:600;">
                                <span class="dashicons dashicons-google" style="font-size:16px;width:16px;height:16px;"></span>
                                <?php esc_html_e('Connect Google', 'seoforge'); ?>
                            </a>
                            <?php else: ?>
                            <?php
                            // actionable fallback. Reaching here means the
                            // site is PRO (we passed the !$sf_can_gsc branch) but
                            // google_auth_url could not resolve an Avakode
                            // site-token — no saved license key and nothing
                            // mintable from Freemius yet. The old copy was a
                            // dead-end sentence; render the canonical
                            // "Connect to Avakode" card instead so the user has a
                            // working next step. Once connected, the site-token
                            // resolves and the Connect Google button appears on
                            // reload.
                            if (class_exists('Forge_Connect') && method_exists('Forge_Connect', 'render_button')):
                                Forge_Connect::render_button('seoforge', '#FF6B35');
                            else:
                            ?>
                            <span style="font-size:14px;color:#94a3b8;"><?php esc_html_e('Connect SEO Forge to Avakode first, then return here to connect Google.', 'seoforge'); ?></span>
                            <?php endif; ?>
                            <?php endif; ?>
                        <?php endif; ?>
                    </div>
                </div>
                <?php if ($gsc_connected): ?>
                <script>
                // Worker-proxied disconnect: wipes the refresh token in D1
                // and the local cached settings. See SEOFORGE_GSC::ajax_disconnect.
                (function(){
                    var btn = document.getElementById('sf-gsc-disconnect');
                    if (!btn) return;
                    function runDisconnect(){
                        btn.disabled = true;
                        btn.textContent = <?php echo wp_json_encode(__('Disconnecting…', 'seoforge')); ?>;
                        var fd = new FormData();
                        fd.append('action', 'seoforge_gsc_disconnect');
                        fd.append('nonce', <?php echo wp_json_encode(wp_create_nonce('seoforge_nonce')); ?>);
                        fetch(ajaxurl, { method: 'POST', body: fd, credentials: 'same-origin' })
                            .then(function(r){ return r.json(); })
                            .then(function(){ window.location.reload(); })
                            .catch(function(){ btn.disabled = false; btn.textContent = <?php echo wp_json_encode(__('Disconnect failed — try again', 'seoforge')); ?>; });
                    }
                    btn.addEventListener('click', function() {
 // use styled confirm modal; native confirm as fallback if sfModal unloaded.
                        var msg = <?php echo wp_json_encode(__('Disconnect Google from SEO Forge? This revokes the OAuth token — you\'ll need to reconnect to fetch rankings again.', 'seoforge')); ?>;
                        var opts = { title: <?php echo wp_json_encode(__('Disconnect Google?', 'seoforge')); ?>, okText: <?php echo wp_json_encode(__('Disconnect', 'seoforge')); ?>, cancelText: <?php echo wp_json_encode(__('Keep connected', 'seoforge')); ?>, variant: 'danger' };
                        if (window.sfModal && window.sfModal.confirm) {
                            window.sfModal.confirm(msg, opts).then(function(ok){ if (ok) runDisconnect(); });
                        } else if (confirm(msg)) {
                            runDisconnect();
                        }
                    });
                })();
                </script>
                <?php endif; ?>

                <!-- XML Sitemap -->
                <div class="sf-card" data-sf-tab="search">
                    <div class="sf-card-head"><span class="dashicons dashicons-media-spreadsheet"></span><h2><?php esc_html_e('XML Sitemap', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'xml-sitemap', __('XML sitemap', 'seoforge'), __('What pages are listed, how to exclude CPTs, how often it refreshes.', 'seoforge')); ?></h2></div>
                    <div class="sf-card-body">
                        <?php
                        $sitemap_settings = get_option('seoforge_sitemap_settings', []);
                        $max_urls = (int) ($sitemap_settings['max_urls'] ?? 1000);
                        ?>
                        <p style="margin:0 0 12px;font-size:14px;color:#64748b;"><?php esc_html_e('Your sitemap:', 'seoforge'); ?> <a href="<?php echo esc_url(home_url('/sitemap_index.xml')); ?>" target="_blank"><?php echo esc_html(home_url('/sitemap_index.xml')); ?></a></p>
                        <p style="margin:0 0 12px;font-size:14px;color:#64748b;"><?php esc_html_e('Which post types appear in the sitemap is controlled by the "Show in search results" toggle next to each Title Template above. Unchecking it both noindexes the post type and removes it from the sitemap.', 'seoforge'); ?></p>
                        <div>
                            <label class="sf-label"><?php esc_html_e('Max URLs per sitemap', 'seoforge'); ?></label>
                            <input type="number" name="sitemap_max_urls" value="<?php echo esc_attr( $max_urls ); ?>" min="100" max="50000" class="sf-input" style="width:120px;">
                        </div>
                    </div>
                </div>

                <!-- Robots.txt -->
                <div class="sf-card" data-sf-tab="search">
                    <div class="sf-card-head"><span class="dashicons dashicons-media-text"></span><h2><?php esc_html_e('Robots.txt', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'hiding-pages-from-search-engines-noindex', __('robots.txt', 'seoforge'), __('What directives do what — Allow, Disallow, Sitemap, Crawl-delay.', 'seoforge')); ?></h2></div>
                    <div class="sf-card-body">
                        <p style="margin:0 0 12px;font-size:14px;color:#64748b;"><?php esc_html_e('Customize your robots.txt. Sitemap URL is auto-appended.', 'seoforge'); ?> <a href="<?php echo esc_url(home_url('/robots.txt')); ?>" target="_blank"><?php esc_html_e('View robots.txt', 'seoforge'); ?> &rarr;</a></p>
                        <?php
                        // (2026-04-22, Azis feedback): textarea goes readonly
                        // in ANY abnormal state. The plugin can't guarantee its
                        // content reaches visitors in either case, so don't pretend
                        // saving helps. Warning UX (pill + details panel) stays so
                        // the user still sees WHY editing is disabled.
                        // 1. EDITABLE (happy path) — on-disk file absent OR ours,
                        // live HTTP /robots.txt matches plugin Settings.
                        // 2. READONLY LOCK — a physical robots.txt we don't own
                        // lives in the site root. Lock-pill + tooltip.
                        // 3. READONLY WARN — our Settings save but the live
                        // /robots.txt URL returns different content (CDN /
                        // reverse proxy / hPanel intercept). Warn-pill +
                        // collapsible "See what visitors actually get" panel.
                        $rx                = SEOFORGE_Robots_Txt::instance();
                        $has_physical      = SEOFORGE_Robots_Txt::physical_exists();
                        $physical_managed  = SEOFORGE_Robots_Txt::physical_managed();
                        $saved_content     = get_option('seoforge_robots_txt', '');
                        if ($physical_managed) {
                            $rx->ensure_managed_file_current($saved_content);
                        }
                        $expected_body     = $rx->build_output($saved_content);
                        $readonly_physical = $has_physical && !$physical_managed;
                        $lock_reason = __(
                            'Editing is disabled because your web server is serving a robots.txt file directly from the site root. To let SEO Forge manage it, delete that file via FTP or your hosting control panel.',
                            'seoforge'
                        );

                        // Divergence detection: HTTP /robots.txt vs what Settings
                        // would produce. Skip when already lock-readonly — that
                        // pill already tells the right story.
                        $divergence = null;  // null = no check run or no issue
                        if (!$readonly_physical) {
                            $http_snap = SEOFORGE_Robots_Txt::http_snapshot();
                            $http_body = (string) ($http_snap['body'] ?? '');
                            if ($http_body !== '') {
                                $http_managed = strpos($http_body, SEOFORGE_Robots_Txt::MANAGED_MARKER) !== false;
                                $normalized_expected = trim(
                                    ($http_managed ? SEOFORGE_Robots_Txt::MANAGED_MARKER . "\n" : '')
                                    . $expected_body
                                );
                                if (trim($http_body) !== $normalized_expected
                                    && trim($http_body) !== trim($expected_body)) {
                                    $divergence = $http_body;
                                }
                            }
                        }

                        // Unified readonly: EITHER on-disk conflict OR server
                        // override means saving here doesn't change what visitors
                        // get. Lock it and make the reason visible via pill.
                        $readonly = $readonly_physical || ($divergence !== null);
                        ?>
                        <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
                            <label class="sf-label" for="seoforge-robots-txt" style="margin-bottom:0;">
                                <?php esc_html_e('robots.txt content', 'seoforge'); ?>
                            </label>
                            <?php if ($readonly_physical): ?>
                                <span class="sf-tip-anchor sf-robots-lock"
                                      tabindex="0"
                                      role="img"
                                      aria-label="<?php echo esc_attr($lock_reason); ?>"
                                      data-sf-tip="<?php echo esc_attr($lock_reason); ?>">
                                    <span class="dashicons dashicons-lock"></span>
                                    <?php esc_html_e('read-only', 'seoforge'); ?>
                                </span>
                            <?php elseif ($divergence !== null): ?>
                                <span class="sf-tip-anchor sf-robots-warn"
                                      tabindex="0"
                                      role="img"
                                      aria-label="<?php esc_attr_e('Live /robots.txt differs from what Settings saves. Something between SEO Forge and visitors (CDN, reverse proxy, hosting control panel) is overriding the response. Expand the panel below to see what visitors actually get.', 'seoforge'); ?>"
                                      data-sf-tip="<?php esc_attr_e('Live /robots.txt differs from what Settings saves. Something between SEO Forge and visitors (CDN, reverse proxy, hosting control panel) is overriding the response.', 'seoforge'); ?>">
                                    <span class="dashicons dashicons-warning"></span>
                                    <?php esc_html_e('server override detected', 'seoforge'); ?>
                                </span>
                            <?php endif; ?>
                        </div>

                        <textarea
                            name="seoforge_robots_txt"
                            id="seoforge-robots-txt"
                            rows="10"
                            class="sf-input"
                            style="line-height:1.55;resize:vertical;<?php echo $readonly ? 'background:#f8fafc;color:#475569;cursor:not-allowed;' : ''; ?>"
                            <?php echo $readonly ? 'readonly' : ''; ?>
                            spellcheck="false"
                            placeholder="<?php echo esc_attr(SEOFORGE_Robots_Txt::get_default()); ?>"
                        ><?php echo esc_textarea($readonly ? $expected_body : $saved_content); ?></textarea>

                        <p style="margin:8px 0 0;font-size:14px;color:#94a3b8;">
                            <?php esc_html_e('Leave empty for WordPress defaults. Sitemap URL is added automatically.', 'seoforge'); ?>
                            <a href="https://support.google.com/webmasters/answer/6062598" target="_blank" rel="noopener" style="margin-left:6px;"><?php esc_html_e('Google Robots.txt tester →', 'seoforge'); ?></a>
                        </p>

                        <?php if ($divergence !== null): ?>
                        <details style="margin-top:10px;padding:12px 14px;background:#fef3c7;border:1px solid #fde68a;border-radius:8px;color:#78350f;font-size:14px;">
                            <summary style="cursor:pointer;font-weight:600;list-style:none;">
                                <span class="dashicons dashicons-warning" style="font-size:16px;width:16px;height:16px;color:#b45309;vertical-align:middle;margin-right:4px;"></span>
                                <?php esc_html_e('Live /robots.txt differs from these Settings — click to compare', 'seoforge'); ?>
                            </summary>
                            <p style="margin:10px 0 6px;line-height:1.5;">
                                <?php esc_html_e('Saving here updates WordPress, but visitors are getting something else. Usually this means a CDN (Cloudflare), a reverse proxy, or the hosting control panel (hPanel / cPanel) has its own robots editor set up. Find and disable that, then save again.', 'seoforge'); ?>
                            </p>
                            <p style="margin:10px 0 4px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;color:#713f12;">
                                <?php esc_html_e('What visitors actually get from /robots.txt:', 'seoforge'); ?>
                            </p>
                            <pre style="margin:0;padding:10px 12px;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;font-family:ui-monospace,monospace;font-size:13px;white-space:pre-wrap;max-height:200px;overflow:auto;"><?php echo esc_html($divergence); ?></pre>
                        </details>
                        <?php endif; ?>
                    </div>
                </div>

                <!-- Breadcrumbs -->
                <?php $bc = SEOFORGE_Breadcrumbs::settings(); ?>
                <div class="sf-card" data-sf-tab="search">
                    <div class="sf-card-head"><span class="dashicons dashicons-arrow-right-alt"></span><h2><?php esc_html_e('Breadcrumbs', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'schema-markup-rich-results-in-google', __('Breadcrumbs', 'seoforge'), __('Rich-result breadcrumbs + shortcode for themes.', 'seoforge')); ?></h2></div>
                    <div class="sf-card-body">
                        <p style="margin:0 0 12px;font-size:14px;color:#64748b;">
                            <?php echo wp_kses( __('JSON-LD <code>BreadcrumbList</code> is always emitted for SEO. Visual rendering is controlled here.', 'seoforge'), array( 'code' => array() ) ); ?>
                        </p>
                        <label style="display:flex;align-items:center;gap:8px;margin-bottom:14px;font-size:14px;">
                            <input type="checkbox" name="seoforge_breadcrumbs[enabled]" value="1" <?php checked(!empty($bc['enabled'])); ?>>
                            <strong><?php esc_html_e('Enable visual breadcrumbs', 'seoforge'); ?></strong>
                        </label>
                        <div class="sf-grid3">
                            <div>
                                <label class="sf-label"><?php esc_html_e('Separator', 'seoforge'); ?></label>
                                <input type="text" class="sf-input" name="seoforge_breadcrumbs[separator]" value="<?php echo esc_attr($bc['separator']); ?>" maxlength="8">
                            </div>
                            <div>
                                <label class="sf-label"><?php esc_html_e('Home text', 'seoforge'); ?></label>
                                <input type="text" class="sf-input" name="seoforge_breadcrumbs[home_text]" value="<?php echo esc_attr($bc['home_text']); ?>" maxlength="60">
                            </div>
                            <div>
                                <label class="sf-label"><?php esc_html_e('Current page', 'seoforge'); ?></label>
                                <label style="display:flex;align-items:center;gap:6px;font-size:14px;padding-top:7px;">
                                    <input type="checkbox" name="seoforge_breadcrumbs[show_current]" value="1" <?php checked(!empty($bc['show_current'])); ?>>
                                    <?php esc_html_e('Show current page as last crumb', 'seoforge'); ?>
                                </label>
                            </div>
                        </div>
                        <div style="margin-top:14px;padding:12px 14px;border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;font-size:14px;line-height:1.6;color:#475569;">
                            <strong><?php esc_html_e('Output on the frontend', 'seoforge'); ?>:</strong>
                            <div style="margin-top:6px;display:grid;grid-template-columns:repeat(2,1fr);gap:8px;">
                                <code style="background:#fff;padding:6px 8px;border-radius:6px;"><?php echo esc_html('[seoforge_breadcrumbs]'); ?></code>
                                <code style="background:#fff;padding:6px 8px;border-radius:6px;"><?php echo esc_html('<?php the_custom_breadcrumbs(); ?>'); ?></code>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Search Appearance (per archive) -->
                <div class="sf-card" data-sf-tab="search">
                    <div class="sf-card-head"><span class="dashicons dashicons-visibility"></span><h2><?php esc_html_e('Search Appearance — Archives', 'seoforge'); ?><?php Forge_Docs_Link::render('seoforge', 'hiding-pages-from-search-engines-noindex', __('Per-archive appearance', 'seoforge'), __('Toggle noindex, set default title/description per CPT or taxonomy.', 'seoforge')); ?></h2></div>
                    <div class="sf-card-body">
                        <p style="margin:0 0 12px;font-size:14px;color:#64748b;"><?php esc_html_e('Control search visibility for post type archives. Per-post and per-category settings are in the editor.', 'seoforge'); ?></p>
                        <?php
                        // the Sitemap card used to initialise $all_types;
                        // removing that card orphaned this card's loop. Seed the
                        // list here locally so the archives toggles keep rendering.
                        $archive_robots = get_option('seoforge_archive_robots', []);
                        $all_types = get_post_types(['public' => true], 'objects');
                        unset($all_types['attachment']);
                        foreach ($all_types as $pt):
                            if (!$pt->has_archive) continue;
                            $ni = !empty($archive_robots[$pt->name]['noindex']);
                        ?>
                        <label style="display:flex;align-items:center;gap:6px;font-size:14px;margin-bottom:6px;">
                            <input type="checkbox" name="archive_noindex[<?php echo esc_attr($pt->name); ?>]" value="1" <?php checked($ni); ?>>
                            <?php /* translators: %s: post type label */ ?>
                            <?php printf(esc_html__('Hide %s archive from search (noindex)', 'seoforge'), esc_html($pt->labels->name)); ?>
                        </label>
                        <?php endforeach; ?>

                        <!-- noindex on paginated archives -->
                        <hr style="border:0;border-top:1px solid #e2e8f0;margin:16px 0;">
                        <label style="display:flex;align-items:flex-start;gap:8px;font-size:14px;">
                            <input type="checkbox"
                                   name="seoforge_noindex_paginated"
                                   value="1"
                                   <?php checked(get_option('seoforge_noindex_paginated', false)); ?>>
                            <span>
                                <strong><?php esc_html_e('Noindex paginated pages', 'seoforge'); ?></strong>
                                <br>
                                <span style="color:#64748b;font-size:14px;">
                                    <?php echo wp_kses( __('Pages 2+ of archives, categories, tags, and search results will output <code>noindex, follow</code>. Keeps crawlers traversing links to articles while preventing duplicate paginated URLs from cluttering search results.', 'seoforge'), array( 'code' => array() ) ); ?>
                                </span>
                            </span>
                        </label>
                    </div>
                </div>

                </div><!-- /.sf-settings-cards -->

                <div class="sf-settings-actions">
                    <button type="submit" name="seoforge_save" class="sf-save-btn"><?php esc_html_e('Save Settings', 'seoforge'); ?></button>
                </div>

                </div><!-- /.sf-settings-content -->
                </div><!-- /.sf-settings-layout -->
            </form>
        </div>

        <style>
        /* instant CSS tooltip. Native title="" has a browser-side
 hover delay (~800-1200ms on macOS Chrome) that made the
 lock/warning pills feel unresponsive. Our own tooltip fires on
 :hover/:focus with zero delay and stays pinned so the user
 can actually read it. */
        .sf-tip-anchor{position:relative;display:inline-flex;align-items:center;gap:4px;font-size:14px;cursor:help;outline:none;}
        .sf-tip-anchor .dashicons{font-size:16px;width:16px;height:16px;}
        .sf-tip-anchor::after{
            content:attr(data-sf-tip);
            position:absolute;left:0;top:calc(100% + 8px);
            max-width:360px;min-width:240px;width:max-content;
            padding:10px 12px;border-radius:8px;
            background:#1e293b;color:#f1f5f9;
            font-size:12px;font-weight:500;line-height:1.5;
            letter-spacing:0;text-transform:none;
            box-shadow:0 8px 24px rgba(0,0,0,0.18);
            z-index:100;
            opacity:0;transform:translateY(-4px);pointer-events:none;
            transition:opacity 0.12s, transform 0.12s;
            white-space:normal;
        }
        .sf-tip-anchor::before{
            content:'';
            position:absolute;left:14px;top:calc(100% + 2px);
            border:6px solid transparent;border-bottom-color:#1e293b;
            z-index:101;
            opacity:0;pointer-events:none;transition:opacity 0.12s;
        }
        .sf-tip-anchor:hover::after,.sf-tip-anchor:focus::after,.sf-tip-anchor:focus-visible::after,
        .sf-tip-anchor:hover::before,.sf-tip-anchor:focus::before,.sf-tip-anchor:focus-visible::before{
            opacity:1;transform:translateY(0);
        }
        .sf-robots-lock{color:#FF6B35;}
        .sf-robots-warn{color:#b45309;}
        .sf-card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,0.06);}
        .sf-card-head{display:flex;align-items:center;gap:10px;padding:16px 20px;border-bottom:1px solid #e2e8f0;background:#f8fafc;}
        .sf-card-head .dashicons{color:#FF6B35;font-size:18px;width:18px;height:18px;}
        .sf-card-head h2{margin:0;font-size:15px;font-weight:600;}
        .sf-card-body{padding:20px;}
        .sf-grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;}
        .sf-label{display:block;font-size:14px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:0.4px;margin-bottom:6px;}
        .sf-select,.sf-input{width:100%;padding:9px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;transition:border-color 0.15s;}
        .sf-select:focus,.sf-input:focus{border-color:#FF6B35;outline:none;box-shadow:0 0 0 3px rgba(255,107,53,0.1);}
        .sf-toggles{display:flex;flex-direction:column;gap:0;}
        .sf-toggle{display:flex;align-items:flex-start;gap:10px;padding:12px 0;border-bottom:1px solid #f1f5f9;cursor:pointer;}
        .sf-toggle:last-child{border-bottom:none;}
        .sf-toggle input{margin-top:3px;accent-color:#FF6B35;}
        .sf-toggle-text{display:flex;flex-direction:column;gap:2px;}
        .sf-toggle-text strong{font-size:14px;color:#1e293b;}
        .sf-toggle-text span{font-size:14px;color:#94a3b8;}
        .sf-save-btn{background:#FF6B35;color:#fff;border:none;border-radius:10px;padding:12px 32px;font-size:14px;font-weight:600;cursor:pointer;box-shadow:0 2px 8px rgba(255,107,53,0.2);transition:all 0.2s;}
        .sf-save-btn:hover{background:#F05E28;transform:translateY(-1px);box-shadow:0 4px 16px rgba(255,107,53,0.3);}
        /* inline variable trigger inside each template field */
        body.forge-admin.sf-admin .sf-tpl-field-wrap{position:relative;}
        body.forge-admin.sf-admin .sf-var-trigger{position:absolute;top:6px;right:6px;width:26px;height:26px;border:0;border-radius:6px;background:#f1f5f9;color:#475569;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all 0.12s;padding:0;}
        body.forge-admin.sf-admin .sf-var-trigger:hover{background:#FFF7ED;color:#FF6B35;}
        body.forge-admin.sf-admin .sf-var-trigger .dashicons{font-size:16px;width:16px;height:16px;line-height:16px;}
        /* Popover */
        .sf-var-popover{position:absolute;z-index:150000;background:#fff;border:1px solid #e2e8f0;border-radius:10px;box-shadow:0 14px 40px rgba(0,0,0,0.16);padding:6px;min-width:240px;max-width:320px;max-height:280px;overflow-y:auto;display:none;}
        .sf-var-popover-row{display:flex;flex-direction:column;align-items:flex-start;gap:2px;padding:8px 10px;width:100%;text-align:left;background:none;border:0;border-radius:6px;cursor:pointer;font-family:inherit;transition:background 0.1s;}
        .sf-var-popover-row:hover{background:#FFF7ED;}
        .sf-var-popover-row code{font-family:"SF Mono","Menlo",monospace;font-size:13px;font-weight:600;color:#FF6B35;background:transparent;padding:0;}
        .sf-var-popover-desc{font-size:12px;color:#64748b;line-height:1.3;}
        .sf-var-popover-empty{padding:12px;text-align:center;color:#94a3b8;font-size:13px;}
        body.forge-admin.sf-admin .sf-tpl-block .sf-input{font-size:14px;}
        body.forge-admin.sf-admin .sf-tpl-block textarea.sf-input{min-height:40px;}
        @media(max-width:782px){.sf-grid3{grid-template-columns:1fr;}.sf-tpl-block>div{grid-template-columns:1fr!important;}}

        /* Settings — vertical sidebar tabs */
        .sf-settings-wrap{max-width:1200px;}
        .sf-settings-layout{display:grid;grid-template-columns:240px 1fr;gap:24px;align-items:start;}
        .sf-settings-nav{position:sticky;top:42px;background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:8px;box-shadow:0 1px 3px rgba(0,0,0,0.06);}
        .sf-settings-nav__select{display:none;width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;}
        .sf-settings-nav__list{margin:0;padding:0;list-style:none;}
        /* SF follow-up: WP admin's core a:hover{text-decoration:underline} was
 bleeding through on Settings sidebar nav items (reporter saw
 "Social" underlined). Force none on base + hover + focus. */
        .sf-settings-nav__item,
        .sf-settings-nav__item:hover,
        .sf-settings-nav__item:focus,
        .sf-settings-nav__item:active{text-decoration:none !important;}
        .sf-settings-nav__item{display:flex;align-items:center;gap:10px;padding:10px 12px;font-size:14px;font-weight:500;color:#475569;border-radius:8px;border:1px solid transparent;transition:all 0.12s;}
        .sf-settings-nav__item:hover{background:#f8fafc;color:#1e293b;}
        .sf-settings-nav__item .dashicons{font-size:18px;width:18px;height:18px;color:#94a3b8;transition:color 0.12s;flex-shrink:0;}
        .sf-settings-nav__item:hover .dashicons{color:#FF6B35;}
        .sf-settings-nav__item.is-active{background:linear-gradient(135deg,#FFF7ED,#FFEDD5);color:#E05520;border-color:#FED7AA;font-weight:600;box-shadow:inset 3px 0 0 #FF6B35;}
        .sf-settings-nav__item.is-active .dashicons{color:#E05520;}
        .sf-settings-nav__item:focus-visible{outline:2px solid #FF6B35;outline-offset:2px;}
        .sf-settings-nav__label{flex:1;}
        .sf-settings-content{min-width:0;}
        .sf-settings-panel-head{margin:0 0 16px;padding:0 0 14px;border-bottom:1px solid #e2e8f0;display:none;}
        .sf-settings-panel-head.is-active{display:block;}
        .sf-settings-panel-head h2{margin:0 0 4px;font-size:18px;font-weight:700;color:#1e293b;}
        .sf-settings-panel-head p{margin:0;font-size:14px;color:#64748b;}
        .sf-settings-cards{display:flex;flex-direction:column;gap:18px;}
        .sf-settings-cards .sf-card[data-sf-tab]{display:none;}
        .sf-settings-cards .sf-card[data-sf-tab].is-active{display:block;animation:sfFade .18s ease-out;}
        @keyframes sfFade{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}
        .sf-settings-actions{margin-top:20px;padding-top:18px;border-top:1px solid #e2e8f0;display:flex;justify-content:flex-end;gap:8px;}
        .sf-settings-nav__item.is-dirty:not(.is-active)::after{content:'';width:6px;height:6px;border-radius:50%;background:#FF6B35;margin-left:auto;flex-shrink:0;}
        .sf-settings-nav__item.is-dirty.is-active::after{content:'';width:6px;height:6px;border-radius:50%;background:#E05520;margin-left:auto;flex-shrink:0;animation:sfPulse 1.4s ease-in-out infinite;}
        @keyframes sfPulse{0%,100%{opacity:1;}50%{opacity:0.4;}}
        @media(max-width:960px){
            .sf-settings-layout{grid-template-columns:1fr;}
            .sf-settings-nav{position:static;}
            .sf-settings-nav__list{display:none;}
            .sf-settings-nav__select{display:block;}
        }
        </style>

        <script>
        // Default OG Image media uploader
        jQuery('#sf-og-image-upload').on('click', function(e) {
            e.preventDefault();
            var frame = wp.media({
                title: '<?php echo esc_js(__('Select Default Social Image', 'seoforge')); ?>',
                button: { text: '<?php echo esc_js(__('Use This Image', 'seoforge')); ?>' },
                multiple: false,
                library: { type: 'image' }
            });
            frame.on('select', function() {
                var attachment = frame.state().get('selection').first().toJSON();
                jQuery('#sf-og-image-url').val(attachment.url);
                jQuery('#sf-og-image-preview').html('<img src="' + attachment.url + '" style="width:100%;height:100%;object-fit:cover;">');
                if (!jQuery('#sf-og-image-remove').length) {
                    jQuery('#sf-og-image-upload').after('<button type="button" id="sf-og-image-remove" style="background:none;border:1px solid #e2e8f0;border-radius:8px;padding:9px 16px;font-size:14px;color:#64748b;cursor:pointer;margin-left:6px;"><?php echo esc_js(__('Remove', 'seoforge')); ?></button>');
                }
            });
            frame.open();
        });
        jQuery(document).on('click', '#sf-og-image-remove', function() {
            jQuery('#sf-og-image-url').val('');
            jQuery('#sf-og-image-preview').html('<span style="color:#94a3b8;font-size:14px;"><?php echo esc_js(__('No image selected', 'seoforge')); ?></span>');
            jQuery(this).remove();
        });

 // Live Preview for Title Templates.
        // Mirror the PHP resolver (SEOFORGE_Meta_Templates::resolve) closely enough
        // that editing a template field or changing the separator updates the
        // preview immediately, without a server round-trip.
        (function(){
            var sampleData = {
                '{{title}}':        <?php echo wp_json_encode(__('Sample post title', 'seoforge')); ?>,
                '{{siteName}}':     <?php echo wp_json_encode(get_bloginfo('name')); ?>,
                '{{siteDesc}}':     <?php echo wp_json_encode(get_bloginfo('description')); ?>,
                '{{excerpt}}':      <?php echo wp_json_encode(__('A short sample excerpt so you can preview how your Description template will look in search results.', 'seoforge')); ?>,
                '{{author}}':       <?php echo wp_json_encode(wp_get_current_user()->display_name ?: __('Author name', 'seoforge')); ?>,
                '{{category}}':     <?php echo wp_json_encode(__('Category', 'seoforge')); ?>,
                '{{date}}':         <?php echo wp_json_encode(date_i18n(get_option('date_format'))); ?>,
                '{{modifiedDate}}': <?php echo wp_json_encode(date_i18n(get_option('date_format'))); ?>,
                '{{currentYear}}':  <?php echo wp_json_encode(gmdate('Y')); ?>,
                '{{currentMonth}}': <?php echo wp_json_encode(gmdate('F')); ?>
            };

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

            function resolve(tpl, separator) {
                if (!tpl) return '';
                var data = jQuery.extend({}, sampleData, { '{{separator}}': separator });
                var out = tpl;
                jQuery.each(data, function(key, val) {
                    out = out.split(key).join(val);
                });
                // Cleanup: collapse double separators, strip leading/trailing, collapse spaces
                var sep = escapeRegex(separator);
                out = out.replace(new RegExp('\\s*' + sep + '\\s*' + sep + '\\s*', 'g'), ' ' + separator + ' ');
                out = out.replace(new RegExp('^\\s*' + sep + '\\s*'), '');
                out = out.replace(new RegExp('\\s*' + sep + '\\s*$'), '');
                out = out.replace(/\s{2,}/g, ' ');
                return jQuery.trim(out);
            }

            function refresh(pt) {
                var sep = jQuery('#sf-tpl-separator').val() || '|';
                var $blocks = pt ? jQuery('.sf-tpl-preview[data-pt="' + pt + '"]') : jQuery('.sf-tpl-preview');
                $blocks.each(function() {
                    var ptName = jQuery(this).data('pt');
                    var tplTitle = jQuery('.sf-tpl-field[data-pt="' + ptName + '"][data-field="title"]').val();
                    var tplDesc  = jQuery('.sf-tpl-field[data-pt="' + ptName + '"][data-field="description"]').val();
                    jQuery(this).find('.sf-preview-title').text(resolve(tplTitle, sep));
                    jQuery(this).find('.sf-preview-desc').text(resolve(tplDesc, sep));
                });
            }

            jQuery(document).on('input change', '.sf-tpl-field', function() {
                refresh(jQuery(this).data('pt'));
            });
            jQuery(document).on('change', '#sf-tpl-separator', function() {
                refresh();
            });
            // Initial render so previews reflect current saved values on page load
            refresh();
        })();

 // contextual variable picker — a popover anchored to each
        // template field's {+} trigger, plus a `{` slash-style autocomplete.
        (function(){
            var VARS = Array.isArray(window.SF_TEMPLATE_VARIABLES) ? window.SF_TEMPLATE_VARIABLES : [];
            var popover = null;
            var activeField = null;
            var filterText = '';
            var slashMode = false;
            var slashStart = -1; // index of the `{` that opened slash-mode

            function ensurePopover() {
                if (popover) return popover;
                popover = document.createElement('div');
                popover.className = 'sf-var-popover';
                document.body.appendChild(popover);
                document.addEventListener('click', function(e){
                    if (!popover || popover.style.display === 'none') return;
                    if (popover.contains(e.target)) return;
                    if (e.target && e.target.closest && e.target.closest('.sf-var-trigger')) return;
                    closePopover();
                });
                document.addEventListener('keydown', function(e){
                    if (!popover || popover.style.display === 'none') return;
                    if (e.key === 'Escape') closePopover();
                });
                return popover;
            }
            function clearNode(n){ while(n && n.firstChild) n.removeChild(n.firstChild); }

            function renderList() {
                var p = ensurePopover();
                clearNode(p);
                var q = (filterText || '').toLowerCase();
                var matched = VARS.filter(function(v){
                    if (!q) return true;
                    return (v.var || '').toLowerCase().indexOf(q) !== -1
                        || (v.desc || '').toLowerCase().indexOf(q) !== -1;
                });
                if (!matched.length) {
                    var empty = document.createElement('div');
                    empty.className = 'sf-var-popover-empty';
                    empty.textContent = 'No variables match';
                    p.appendChild(empty);
                    return;
                }
                matched.forEach(function(v){
                    var row = document.createElement('button');
                    row.type = 'button';
                    row.className = 'sf-var-popover-row';
                    var code = document.createElement('code');
                    code.textContent = v.var;
                    var desc = document.createElement('span');
                    desc.className = 'sf-var-popover-desc';
                    desc.textContent = v.desc || '';
                    row.appendChild(code);
                    row.appendChild(desc);
                    row.addEventListener('click', function(e){
                        e.preventDefault();
                        insertVar(v.var);
                    });
                    p.appendChild(row);
                });
            }

            function openPopover(field, anchor) {
                activeField = field;
                var p = ensurePopover();
                renderList();
                var r = anchor.getBoundingClientRect();
                p.style.display = 'block';
                p.style.top  = (window.scrollY + r.bottom + 6) + 'px';
                p.style.left = (window.scrollX + r.left) + 'px';
                // Clamp to viewport
                var maxLeft = window.innerWidth - 260;
                if (parseFloat(p.style.left) > maxLeft) p.style.left = maxLeft + 'px';
            }

            function closePopover() {
                if (popover) popover.style.display = 'none';
                slashMode = false;
                filterText = '';
                slashStart = -1;
            }

            function insertVar(text) {
                if (!activeField) { closePopover(); return; }
                var input = activeField;
                var start = input.selectionStart;
                var end   = input.selectionEnd;
                var val   = input.value || '';
                if (slashMode && slashStart >= 0) {
                    // Replace the `{<typedFilter>` we were tracking with the full var.
                    start = slashStart;
                }
                input.value = val.substring(0, start) + text + val.substring(end);
                var newPos = start + text.length;
                input.selectionStart = input.selectionEnd = newPos;
                input.focus();
                jQuery(input).trigger('input');
                closePopover();
            }

            // Clicking the {+} trigger next to a field
            jQuery(document).on('click', '.sf-var-trigger', function(e){
                e.preventDefault();
                var $wrap = jQuery(this).closest('.sf-tpl-field-wrap');
                var field = $wrap.find('.sf-tpl-field')[0];
                if (!field) return;
                slashMode = false;
                filterText = '';
                openPopover(field, this);
            });

            // Slash-command inside template fields: `{` opens the picker and
            // subsequent characters filter the list.
            jQuery(document).on('input', '.sf-tpl-field', function(){
                var field = this;
                var pos = field.selectionStart || 0;
                var val = field.value || '';
                if (!slashMode) {
                    if (val.charAt(pos - 1) === '{') {
                        slashMode = true;
                        slashStart = pos - 1;
                        filterText = '';
                        activeField = field;
                        // Anchor popover under the field (trigger-like)
                        var trigger = field.parentNode.querySelector('.sf-var-trigger') || field;
                        openPopover(field, trigger);
                    }
                    return;
                }
                // Already in slash mode — derive filter from text after `{`
                if (pos < slashStart + 1) { closePopover(); return; }
                var candidate = val.substring(slashStart + 1, pos);
                if (candidate.indexOf(' ') !== -1 || candidate.indexOf('}') !== -1) {
                    closePopover();
                    return;
                }
                filterText = candidate;
                renderList();
            });

            jQuery(document).on('keydown', '.sf-tpl-field', function(e){
                if (!slashMode) return;
                if (e.key === 'Escape') { closePopover(); }
            });
        })();

        jQuery('#seoforge-bulk-analyze').on('click', function() {
            var $btn = jQuery(this);
            $btn.prop('disabled', true);
            jQuery('#seoforge-bulk-progress').show();

            jQuery.get(seoforgeAdmin.restUrl + '/dashboard', { _wpnonce: seoforgeAdmin.restNonce }, function() {
                // Get all published post IDs
                jQuery.get('/wp-json/wp/v2/posts?status=publish&per_page=100&_fields=id', function(posts) {
                    var total = posts.length, done = 0;
                    function next() {
                        if (done >= total) {
                            jQuery('#seoforge-bulk-status').text('Done! ' + total + ' posts analyzed.');
                            jQuery('#seoforge-bulk-bar').css({width: '100%', background: '#059669'});
                            $btn.prop('disabled', false);
                            return;
                        }
                        var id = posts[done].id;
                        jQuery.get(seoforgeAdmin.restUrl + '/analyze/' + id, { _wpnonce: seoforgeAdmin.restNonce }, function() {
                            done++;
                            var pct = Math.round(done / total * 100);
                            jQuery('#seoforge-bulk-bar').css('width', pct + '%');
                            jQuery('#seoforge-bulk-status').text(done + ' / ' + total);
                            next();
                        });
                    }
                    next();
                });
            });
        });

 // Settings vertical tabs.
        // - Active tab persists across reloads via localStorage.
        // - URL hash (#sf-tab-general) on load picks the tab and updates as you switch.
        // - Validation errors / save_errors anywhere force-jump to the panel containing them.
        // - All form fields stay in DOM so the existing single <form> save still works.
        (function(){
            if (window.seoforgeInitSettingsTabs) {
                window.seoforgeInitSettingsTabs();
                return;
            }
            var $ = jQuery;
            var TAB_KEY = 'sf_settings_tab_v1';
            var DEFAULT_TAB = 'general';
            var $items   = $('.sf-settings-nav__item');
            var $cards   = $('.sf-settings-cards .sf-card[data-sf-tab]');
            var $heads   = $('[data-sf-panel-head]');
            var $select  = $('.sf-settings-nav__select');
            var validTabs = $items.map(function(){ return $(this).data('sf-tab'); }).get();

            function setActive(tab, opts){
                opts = opts || {};
                if (validTabs.indexOf(tab) === -1) tab = DEFAULT_TAB;
                $items.removeClass('is-active').attr('aria-selected','false').filter('[data-sf-tab="'+tab+'"]').addClass('is-active').attr('aria-selected','true');
                $cards.removeClass('is-active').filter('[data-sf-tab="'+tab+'"]').addClass('is-active');
                $heads.removeClass('is-active').filter('[data-sf-panel-head="'+tab+'"]').addClass('is-active');
                if ($select.val() !== tab) $select.val(tab);
                try { localStorage.setItem(TAB_KEY, tab); } catch(e){}
                if (opts.updateHash !== false) {
                    if (history.replaceState) history.replaceState(null,'','#sf-tab-'+tab);
                    else window.location.hash = 'sf-tab-'+tab;
                }
            }

 // prefer URL hash, then default. The previous code used
            // localStorage as a second-tier fallback, which made the menu link
            // (no hash) open whatever tab the user was last on — confusing
            // for a top-level nav click. We still honour localStorage AFTER
            // a save-redirect (WP round-trips through settings-updated=true
            // stripping the hash) so the user returns to the tab they
            // saved from.
            var initial = DEFAULT_TAB;
            var hashMatch = (location.hash || '').match(/^#sf-tab-([a-z0-9-]+)/i);
            var isAfterSave = /[?&]settings-updated=true/.test(location.search);
            if (hashMatch && validTabs.indexOf(hashMatch[1]) !== -1) {
                initial = hashMatch[1];
            } else if (isAfterSave) {
                try {
                    var saved = localStorage.getItem(TAB_KEY);
                    if (saved && validTabs.indexOf(saved) !== -1) initial = saved;
                } catch(e){}
            }
            setActive(initial, {updateHash:false});

            $items.on('click', function(e){
                e.preventDefault();
                setActive($(this).data('sf-tab'));
                $('.sf-settings-content')[0].scrollIntoView({behavior:'smooth', block:'start'});
            });
            $select.on('change', function(){ setActive($(this).val()); });

            // Keyboard: ArrowUp/Down moves focus inside the nav (ARIA pattern).
            $items.on('keydown', function(e){
                var idx = $items.index(this);
                var next = null;
                if (e.key === 'ArrowDown' || e.key === 'ArrowRight') next = $items.eq((idx+1) % $items.length);
                else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') next = $items.eq((idx-1+$items.length) % $items.length);
                else if (e.key === 'Home') next = $items.first();
                else if (e.key === 'End')  next = $items.last();
                if (next && next.length) { e.preventDefault(); next.focus().trigger('click'); }
            });

            // If a notice (.notice-error) is rendered inside a panel, force that panel open.
            var $errorPanel = $cards.has('.notice-error, .form-invalid').first();
            if ($errorPanel.length) setActive($errorPanel.data('sf-tab'));

            // Visual cue: badge the tab when fields inside are dirty (helps when
            // user switches tabs with unsaved changes).
            $('.sf-settings-cards :input').on('input change', function(){
                var tab = $(this).closest('[data-sf-tab]').data('sf-tab');
                if (!tab) return;
                $items.filter('[data-sf-tab="'+tab+'"]').addClass('is-dirty');
            });
            $('.sf-settings-form').on('submit', function(){
                $items.removeClass('is-dirty');
            });
        })();
        </script>
        <?php
    }
}
