<?php
/**
 * SEOForge — the full post-edit metabox view.
 *
 * F14 final split (2026-04-21). The inline render_meta_box body moves
 * here (~580 lines). class-analyzer.php keeps the hook registration plus
 * the thin delegator stub plus the analyzer-state methods (analyze,
 * cache, stats).
 *
 * Shape: one static entry, render($analyzer, $post). The view receives
 * the analyzer instance so it can call back for read-only helpers:
 * get_cached, has_editable_content, resolve_content_html,
 * prepare_live_scoring_context, render_schema_builder.
 *
 * SERP + Social tabs delegate to SEOFORGE_Preview_View.
 * Schema tab delegates to SEOFORGE_Schema_Builder_View.
 *
 * @package SEOForge
 */

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

if (!class_exists('SEOFORGE_Metabox_View')) {

class SEOFORGE_Metabox_View {
    /**
 * Render the SEO Forge metabox body.
 *
 * @param SEOFORGE_Analyzer $analyzer Caller passes itself so the view
 * reaches back for the helper methods noted at the top of this
 * file.
 * @param WP_Post $post
 */
    public static function render($analyzer, $post) {
        if (!$post || !is_object($post) || !isset($post->ID)) return;

        // Always show a freshly-computed, rule-based score + Issues list. We do
        // NOT read the cached score for display: the cache row (written on save
        // for the dashboard/audit) can be stale — e.g. an older AI-scored row —
        // and the metabox number must always match the deterministic live
        // scorer, never surprise the user with an outdated value. Read-only:
        // no AI call, no DB write (auto_analyze persists the cache on save).
        $seed = $analyzer->analyze($post->ID, false, false);
        if (is_array($seed)) {
            $score  = (int) $seed['score'];
            $issues = is_array($seed['issues']) ? $seed['issues'] : [];
        } else {
            $analysis = $analyzer->get_cached($post->ID);
            $score    = $analysis ? (int) $analysis->score : 0;
            $decoded  = $analysis ? json_decode($analysis->issues, true) : [];
            $issues   = is_array($decoded) ? $decoded : [];
        }
        $meta_title = get_post_meta($post->ID, '_seoforge_meta_title', true);
        $meta_desc = get_post_meta($post->ID, '_seoforge_meta_description', true);
        $keyword = get_post_meta($post->ID, '_seoforge_focus_keyword', true);

        $color = $score >= 80 ? '#059669' : ($score >= 50 ? '#D97706' : '#DC2626');
        // SEOFORGE_AI_Client lives in seoforge-pro (post-split). On Free-only
        // sites the class is absent — AI buttons stay hidden via $ai_pro below.
        $ai_configured = class_exists('SEOFORGE_AI_Client')
            ? SEOFORGE_AI_Client::instance()->is_configured()
            : false;
        // SF-QA-001: AI buttons are gated on BOTH a valid API key AND a PRO plan.
        // $ai_configured alone only tells us the key is set — it does not mean
        // the user is entitled to consume AI credits. Free users who happen to
        // have an API key configured must NOT see or be able to trigger AI features.
        $ai_pro = $ai_configured && SEOFORGE_License::can('ai_features');
        // Free users (no AI entitlement) still SEE the AI buttons, but
        // locked — a click opens the upsell modal instead of spending credits.
        // This is distinct from the "Pro entitled but no API key yet" state,
        // which stays hidden (that's a config prompt, not a paywall).
        $ai_locked = !SEOFORGE_License::can('ai_features');
        ?>
        <div class="seoforge-metabox seoforge-metabox--wide">
            <div class="sf-mb-header">
                <div class="seoforge-score-circle" id="sf-score-circle" style="border-color:<?php echo esc_attr( $color ); ?>">
                    <span id="sf-score-value" style="color:<?php echo esc_attr( $color ); ?>"><?php echo esc_html( $score ); ?></span>
                </div>
                <div class="sf-mb-header-text">
                    <div class="sf-mb-header-title"><?php esc_html_e('SEO Forge', 'seoforge'); ?></div>
                    <div class="sf-mb-header-sub">
                        <?php /* translators: first %d is the numeric score 0-100 */ ?>
                        <span id="sf-score-label"><?php printf( esc_html__('Overall score %d / 100', 'seoforge'), (int) $score); ?></span>
                        <?php if ($ai_configured): ?> &middot; <span class="sf-ai-badge-inline"><span class="dashicons dashicons-superhero-alt"></span> AI-Powered</span><?php endif; ?>
                    </div>
                </div>
            </div>

            <?php
            // /Seed the live scorer from stored content so the JS
            // can react without a round-trip. The admin script refreshes body
            // metrics from the current editor state before each score update.
            $sf_live = $analyzer->prepare_live_scoring_context($post);
            ?>
            <script>window.SEOForgeLive = <?php echo wp_json_encode($sf_live); ?>;</script>

            <?php
            // per-post Social Media Image (og:image) override.
            $og_image_id  = (int) get_post_meta($post->ID, '_seoforge_og_image_id', true);
            $og_image_url = $og_image_id ? wp_get_attachment_image_url($og_image_id, 'large') : '';
            if (!$og_image_url) {
                $og_image_url = (string) get_post_meta($post->ID, '_seoforge_og_image', true);
            }
            $thumb_url = class_exists('SEOFORGE_Meta_Generator')
                ? SEOFORGE_Meta_Generator::resolve_social_image_url($post->ID, 'large')
                : ($og_image_url ?: (get_the_post_thumbnail_url($post->ID, 'medium') ?: get_option('seoforge_default_og_image', '')));
            $noindex   = get_post_meta($post->ID, '_seoforge_noindex', true);
            $nofollow  = get_post_meta($post->ID, '_seoforge_nofollow', true);
            $canonical_url = class_exists('SEOFORGE_Meta_Generator')
                ? SEOFORGE_Meta_Generator::get_custom_canonical_url($post->ID)
                : esc_url((string) get_post_meta($post->ID, '_seoforge_canonical_url', true));

            // per-post Social title / description overrides.
            // Empty = fall back to the SEO tab's meta_title / meta_description.
            $og_title         = (string) get_post_meta($post->ID, '_seoforge_og_title', true);
            $og_description   = (string) get_post_meta($post->ID, '_seoforge_og_description', true);
            $twitter_title    = (string) get_post_meta($post->ID, '_seoforge_twitter_title', true);
            $twitter_desc     = (string) get_post_meta($post->ID, '_seoforge_twitter_description', true);
            ?>

            <?php
            // Tabs for the SEO metabox.
            // UX rationale: the old 2-column dump made every author scroll past
            // schema, video, and noindex toggles to reach the 80% case (title +
            // description + focus keyword). Now the SEO tab opens by default
            // and contains only what every post needs; everything else is one
            // click away on a labelled tab. Tabs persist per user in
            // localStorage so editors land where they left off.
            $issues_count = is_array($issues) ? count($issues) : 0;
            ?>
            <div class="sf-mb-tabs" role="tablist" aria-label="<?php esc_attr_e('SEO Forge sections', 'seoforge'); ?>">
                <button type="button" class="sf-mb-tab" data-sf-mbtab="seo" id="sf-mb-tab-seo" role="tab" aria-selected="true" aria-controls="sf-mb-panel-seo" tabindex="0">
                    <span class="dashicons dashicons-chart-line"></span><?php esc_html_e('SEO', 'seoforge'); ?>
                    <?php if ($issues_count): ?><span class="sf-mb-tab__badge sf-mb-tab__badge--issues" id="sf-mb-issues-badge"><?php echo esc_html( $issues_count ); ?></span><?php endif; ?>
                </button>
                <button type="button" class="sf-mb-tab" data-sf-mbtab="social" id="sf-mb-tab-social" role="tab" aria-selected="false" aria-controls="sf-mb-panel-social" tabindex="-1">
                    <span class="dashicons dashicons-share"></span><?php esc_html_e('Social', 'seoforge'); ?>
                </button>
                <button type="button" class="sf-mb-tab" data-sf-mbtab="schema" id="sf-mb-tab-schema" role="tab" aria-selected="false" aria-controls="sf-mb-panel-schema" tabindex="-1">
                    <span class="dashicons dashicons-screenoptions"></span><?php esc_html_e('Schema', 'seoforge'); ?>
                </button>
                <button type="button" class="sf-mb-tab" data-sf-mbtab="advanced" id="sf-mb-tab-advanced" role="tab" aria-selected="false" aria-controls="sf-mb-panel-advanced" tabindex="-1">
                    <span class="dashicons dashicons-admin-tools"></span><?php esc_html_e('Advanced', 'seoforge'); ?>
                    <?php if ($noindex): ?><span class="sf-mb-tab__badge sf-mb-tab__badge--warn" title="noindex">noindex</span><?php endif; ?>
                </button>
            </div>

            <!-- ========== TAB: SEO (default) ========== -->
            <div class="sf-mb-panel" data-sf-mbpanel="seo" id="sf-mb-panel-seo" role="tabpanel" aria-labelledby="sf-mb-tab-seo">
                <div class="sf-field-group">
                    <div class="sf-field-header">
                        <strong><?php esc_html_e('Focus Keyword', 'seoforge'); ?><?php if (class_exists('Forge_Docs_Link')) Forge_Docs_Link::render('seoforge', 'writing-a-good-focus-keyword', __('Focus keyword', 'seoforge'), __('The search phrase the analyzer measures against title / URL / body.', 'seoforge')); ?></strong>
                        <?php if ($ai_pro): ?>
                        <button type="button" class="sf-ai-gen-btn" data-type="keyword" data-post="<?php echo esc_attr( $post->ID ); ?>" title="<?php esc_attr_e('Suggest focus keyword from content', 'seoforge'); ?>">
                            AI <?php echo Forge_Credits_Widget::render_badge_html('generate_meta', 'sf'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                        </button>
                        <?php elseif ($ai_locked): echo SEOFORGE_License::ai_lock_html('meta', 'AI', 'pill'); /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup */ endif; ?>
                    </div>
                    <input type="text" name="seoforge_focus_keyword" id="sf-keyword" value="<?php echo esc_attr($keyword); ?>" class="widefat" placeholder="e.g. wordpress seo">
                </div>

                <div class="sf-field-group">
                    <div class="sf-field-header">
                        <strong><?php esc_html_e('SEO Title', 'seoforge'); ?><?php if (class_exists('Forge_Docs_Link')) Forge_Docs_Link::render('seoforge', 'writing-seo-titles-and-descriptions', __('SEO title', 'seoforge'), __('Shown in Google results. ~60 chars; include the focus keyword once.', 'seoforge')); ?></strong>
                        <?php if ($ai_pro): ?>
                        <button type="button" class="sf-ai-gen-btn" data-type="title" data-post="<?php echo esc_attr( $post->ID ); ?>" title="<?php esc_attr_e('Spends AI credits', 'seoforge'); ?>">
                            AI <?php echo Forge_Credits_Widget::render_badge_html('generate_meta', 'sf'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                        </button>
                        <?php elseif ($ai_locked): echo SEOFORGE_License::ai_lock_html('meta', 'AI', 'pill'); /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup */ endif; ?>
                    </div>
                    <?php // no maxlength — never hard-block typing/paste. The
                    // .sf-length-meter below warns visually past the ideal length
                    // (counter turns red, bar fills, "Too long" hint) and the
                    // Google preview truncates with ellipsis. Server saves in full. ?>
                    <input type="text" name="seoforge_meta_title" id="sf-meta-title" value="<?php echo esc_attr($meta_title); ?>" class="widefat">
                    <div class="sf-length-meter" data-target="seoforge_meta_title" data-ideal="60" data-max="70">
                        <div class="sf-length-meter__track"><div class="sf-length-meter__fill"></div></div>
                        <div class="sf-length-meter__row">
                            <small class="seoforge-char-count"><?php echo esc_html( mb_strlen($meta_title) ); ?>/60</small>
                            <small class="sf-length-meter__hint" aria-live="polite" data-short="<?php esc_attr_e('Too short', 'seoforge'); ?>" data-ok="<?php esc_attr_e('Almost there', 'seoforge'); ?>" data-ideal-label="<?php esc_attr_e('Ideal', 'seoforge'); ?>" data-long="<?php esc_attr_e('Too long — Google will truncate it', 'seoforge'); ?>"></small>
                        </div>
                    </div>
                </div>

                <div class="sf-field-group">
                    <div class="sf-field-header">
                        <strong><?php esc_html_e('Meta Description', 'seoforge'); ?><?php if (class_exists('Forge_Docs_Link')) Forge_Docs_Link::render('seoforge', 'writing-seo-titles-and-descriptions', __('Meta description', 'seoforge'), __('Snippet under the SERP title. ~160 chars, sell the click without clickbait.', 'seoforge')); ?></strong>
                        <?php if ($ai_pro): ?>
                        <button type="button" class="sf-ai-gen-btn" data-type="description" data-post="<?php echo esc_attr( $post->ID ); ?>" title="<?php esc_attr_e('Spends AI credits', 'seoforge'); ?>">
                            AI <?php echo Forge_Credits_Widget::render_badge_html('generate_meta', 'sf'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                        </button>
                        <?php elseif ($ai_locked): echo SEOFORGE_License::ai_lock_html('meta', 'AI', 'pill'); /* phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup */ endif; ?>
                    </div>
                    <?php // no maxlength — advisor, not gatekeeper. See note on the title field above. ?>
                    <textarea name="seoforge_meta_description" id="sf-meta-desc" class="widefat" rows="3"><?php echo esc_textarea($meta_desc); ?></textarea>
                    <div class="sf-length-meter" data-target="seoforge_meta_description" data-ideal="160" data-max="170">
                        <div class="sf-length-meter__track"><div class="sf-length-meter__fill"></div></div>
                        <div class="sf-length-meter__row">
                            <small class="seoforge-char-count"><?php echo esc_html( mb_strlen($meta_desc) ); ?>/160</small>
                            <small class="sf-length-meter__hint" aria-live="polite" data-short="<?php esc_attr_e('Too short', 'seoforge'); ?>" data-ok="<?php esc_attr_e('Almost there', 'seoforge'); ?>" data-ideal-label="<?php esc_attr_e('Ideal', 'seoforge'); ?>" data-long="<?php esc_attr_e('Too long — Google will truncate it', 'seoforge'); ?>"></small>
                        </div>
                    </div>
                </div>

                <?php if ($ai_pro): ?>
                <button type="button" class="sf-ai-gen-both" data-post="<?php echo esc_attr( $post->ID ); ?>">
                    AI Keyword + Title + Description <?php echo Forge_Credits_Widget::render_badge_sum_html(['generate_meta', 'generate_meta', 'generate_meta'], 'sf'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                </button>
                <?php elseif ($ai_locked): ?>
                <?php /* Free upsell entry point for "Generate AI Meta". */ ?>
                <?php echo SEOFORGE_License::ai_lock_html('meta', '✨ ' . __('Generate AI Meta', 'seoforge'), 'block'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                <?php endif; ?>

                <?php // F14 2026-04-21: SERP preview extracted to class-seo-preview.php
                SEOFORGE_Preview_View::render_serp([
                    'post'       => $post,
                    'meta_title' => $meta_title,
                    'meta_desc'  => $meta_desc,
                ]);
                ?>

                <?php
                // phase 2 — Google index status panel. Only on published
                // posts with a resolvable permalink and an active GSC connection;
                // otherwise we'd render an empty card or a retry button against
                // nothing. The JS lazily fetches (cached 6h server-side).
                $gsc_connected = class_exists('SEOFORGE_GSC') && SEOFORGE_GSC::instance()->is_connected();
                if ($gsc_connected && $post && $post->post_status === 'publish' && get_permalink($post)):
                ?>
                <div class="sf-field-group sf-gsc-inspect-wrap" style="margin-top:16px;padding:14px 16px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;">
                    <div class="sf-field-header" style="display:flex;align-items:center;justify-content:space-between;gap:10px;">
                        <strong style="font-size:13px;"><span class="dashicons dashicons-search" style="color:#FF6B35;font-size:14px;width:14px;height:14px;vertical-align:text-bottom;"></span> <?php esc_html_e('Google index status', 'seoforge'); ?></strong>
                        <button type="button" class="button button-small sf-gsc-inspect-btn" data-post="<?php echo (int) $post->ID; ?>">
                            <?php esc_html_e('Check status', 'seoforge'); ?>
                        </button>
                    </div>
                    <div class="sf-gsc-inspect-body" style="margin-top:10px;min-height:24px;font-size:13px;color:#64748b;">
                        <?php esc_html_e('Click Check status to pull the current coverage state, last crawl, and mobile / rich-results verdict from Google Search Console for this URL.', 'seoforge'); ?>
                    </div>
                </div>
                <script>
                (function(){
                    var root = document.currentScript.previousElementSibling;
                    if (!root || !root.classList || !root.classList.contains('sf-gsc-inspect-wrap')) return;
                    var btn  = root.querySelector('.sf-gsc-inspect-btn');
                    var body = root.querySelector('.sf-gsc-inspect-body');
                    if (!btn || !body) return;
                    function esc(s) { return String(s == null ? '' : s); }
                    function pill(label, value, color) {
                        var s = document.createElement('span');
                        s.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:12px;font-size:12px;font-weight:600;margin:2px 6px 2px 0;background:' + color.bg + ';color:' + color.fg + ';';
                        s.textContent = label + ': ' + esc(value);
                        return s;
                    }
                    function verdictColor(v) {
                        v = (v || '').toUpperCase();
                        if (v === 'PASS') return { bg: '#ecfdf5', fg: '#047857' };
                        if (v === 'NEUTRAL') return { bg: '#fffbeb', fg: '#92400e' };
                        if (v === 'FAIL') return { bg: '#fef2f2', fg: '#b91c1c' };
                        return { bg: '#f1f5f9', fg: '#475569' };
                    }
                    function clear(n){ while(n.firstChild) n.removeChild(n.firstChild); }

                    btn.addEventListener('click', function(){
                        btn.disabled = true;
                        btn.textContent = <?php echo wp_json_encode(__('Checking…', 'seoforge')); ?>;
                        var fd = new FormData();
                        fd.append('action', 'seoforge_gsc_inspect');
                        fd.append('nonce',  <?php echo wp_json_encode(wp_create_nonce('seoforge_nonce')); ?>);
                        fd.append('post_id', btn.getAttribute('data-post'));
                        fetch(ajaxurl, { method: 'POST', body: fd, credentials: 'same-origin' })
                            .then(function(r){ return r.json(); })
                            .then(function(resp){
                                btn.disabled = false;
                                btn.textContent = <?php echo wp_json_encode(__('Re-check', 'seoforge')); ?>;
                                clear(body);
                                if (!resp.success || !resp.data) {
 // v3 (2026-04-27): when Google reports
                                    // the Search Console API is disabled on the
                                    // GCP project, the response carries a code
                                    // 'api_disabled' + the exact enable URL Google
                                    // handed back. Render a friendly card with a
                                    // one-click CTA instead of dumping the raw
                                    // 400 sentence on the user.
                                    if (resp.data && resp.data.code === 'api_disabled' && resp.data.enable_url) {
                                        var card = document.createElement('div');
                                        card.style.cssText = 'background:#fff7ed;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;font-size:13px;color:#78350f;';
                                        var title = document.createElement('div');
                                        title.style.cssText = 'font-weight:600;margin-bottom:4px;';
                                        title.textContent = <?php echo wp_json_encode(__('Google Search Console API needs to be enabled', 'seoforge')); ?>;
                                        card.appendChild(title);
                                        var hint = document.createElement('div');
                                        hint.style.cssText = 'margin-bottom:8px;line-height:1.4;';
                                        hint.textContent = <?php echo wp_json_encode(__('Open the link below, click Enable, wait one minute, then click Re-check.', 'seoforge')); ?>;
                                        card.appendChild(hint);
                                        var openA = document.createElement('a');
                                        openA.href = /^https?:\/\//i.test(resp.data.enable_url || '') ? resp.data.enable_url : '#';
                                        openA.target = '_blank';
                                        openA.rel = 'noopener';
                                        openA.className = 'button button-primary';
                                        openA.style.cssText = 'background:#FF6B35;border-color:#FF6B35;color:#fff;margin-right:6px;';
                                        openA.textContent = <?php echo wp_json_encode(__('Open Google Cloud Console', 'seoforge')); ?>;
                                        card.appendChild(openA);
                                        body.appendChild(card);
                                        return;
                                    }
                                    var e = document.createElement('div');
                                    e.style.color = '#b91c1c';
                                    e.textContent = (resp.data && resp.data.message) || <?php echo wp_json_encode(__('Could not fetch index status.', 'seoforge')); ?>;
                                    body.appendChild(e);
                                    return;
                                }
                                var d = resp.data;
                                var row = document.createElement('div');
                                row.style.cssText = 'display:flex;flex-wrap:wrap;align-items:center;';
                                if (d.verdict)        row.appendChild(pill(<?php echo wp_json_encode(__('Coverage', 'seoforge')); ?>, d.verdict,        verdictColor(d.verdict)));
                                if (d.coverage)       row.appendChild(pill(<?php echo wp_json_encode(__('State', 'seoforge')); ?>,    d.coverage,       { bg: '#f1f5f9', fg: '#475569' }));
                                if (d.indexing_state) row.appendChild(pill(<?php echo wp_json_encode(__('Indexing', 'seoforge')); ?>, d.indexing_state, { bg: '#f1f5f9', fg: '#475569' }));
                                if (d.mobile_verdict) row.appendChild(pill(<?php echo wp_json_encode(__('Mobile', 'seoforge')); ?>,   d.mobile_verdict, verdictColor(d.mobile_verdict)));
                                if (d.rich_verdict)   row.appendChild(pill(<?php echo wp_json_encode(__('Rich results', 'seoforge')); ?>, d.rich_verdict, verdictColor(d.rich_verdict)));
                                body.appendChild(row);
                                if (d.last_crawl) {
                                    var crawl = document.createElement('div');
                                    crawl.style.cssText = 'margin-top:6px;font-size:12px;color:#64748b;';
                                    crawl.textContent = <?php echo wp_json_encode(__('Last crawled:', 'seoforge')); ?> + ' ' + new Date(d.last_crawl).toLocaleString();
                                    body.appendChild(crawl);
                                }
                                if (d.canonical_user && d.canonical_goog && d.canonical_user !== d.canonical_goog) {
                                    var canonical = document.createElement('div');
                                    canonical.style.cssText = 'margin-top:6px;font-size:12px;color:#b45309;';
                                    canonical.textContent = <?php echo wp_json_encode(__('Canonical mismatch — Google picked a different URL than you declared.', 'seoforge')); ?>;
                                    body.appendChild(canonical);
                                }
                                if (d.inspection_url) {
                                    var link = document.createElement('a');
                                    link.href = /^https?:\/\//i.test(d.inspection_url || '') ? d.inspection_url : '#';
                                    link.target = '_blank';
                                    link.rel = 'noopener';
                                    link.style.cssText = 'display:inline-block;margin-top:6px;font-size:12px;color:#2563eb;';
                                    link.textContent = <?php echo wp_json_encode(__('Open in Search Console →', 'seoforge')); ?>;
                                    body.appendChild(link);
                                }
                            })
                            .catch(function(){
                                btn.disabled = false;
                                btn.textContent = <?php echo wp_json_encode(__('Check status', 'seoforge')); ?>;
                                clear(body);
                                var e = document.createElement('div');
                                e.style.color = '#b91c1c';
                                e.textContent = <?php echo wp_json_encode(__('Network error — try again.', 'seoforge')); ?>;
                                body.appendChild(e);
                            });
                    });
                })();
                </script>
                <?php endif; ?>

                <?php
                // always render the Issues container so the live scorer
                // has a DOM target on posts with no cached analysis yet. $issues
                // is seeded (rule-based) for unsaved posts and read from cache
                // for saved ones; both share this render path. When there are
                // zero issues the <details> + Fix-all are hidden and the JS
                // toggles them as issues come and go while the user edits.
                $has_editable = $analyzer->has_editable_content($post);
                $issues = is_array($issues) ? $issues : [];
                // pure click-through heuristics (numbers / power words in
                // the title) are marked optional by the scorer. Split them into a
                // separate "Optimization ideas" list so they don't count as
                // problems or inflate the red Issues badge.
                $problems = array_values(array_filter($issues, function ($i) { return empty($i['optional']); }));
                $ideas    = array_values(array_filter($issues, function ($i) { return !empty($i['optional']); }));
                $issues_total = count($problems);
                $fixable_issues = [];
                if ($ai_pro) {
                    foreach ($problems as $_iss) {
                        if (SEOFORGE_Analyzer::is_issue_ai_fixable($_iss['message'], $has_editable)) {
                            $fixable_issues[] = $_iss;
                        }
                    }
                }
                // Sum real per-issue cost (1 for keyword-gen, 2 for fix_issue)
                $total_cost = 0;
                foreach ($fixable_issues as $_iss) {
                    $msg = $_iss['message'];
                    $total_cost += SEOFORGE_Analyzer::fix_credit_cost($msg);
                }
                ?>
                <div style="position:relative;">
                <?php if ($ai_pro): ?>
                    <!-- Fix-all placed OUTSIDE <summary> so a click on it
 doesn't toggle the details element. Absolute-positioned
 to line up visually with the summary row. Hidden until
 the live scorer reports more than one fixable issue. -->
                    <button type="button"
                            class="sf-fix-btn sf-fix-all-btn"
                            data-post="<?php echo esc_attr( $post->ID ); ?>"
                            data-issues="<?php echo esc_attr(wp_json_encode(array_map(function($i){ return $i['message']; }, $fixable_issues))); ?>"
                            title="<?php esc_attr_e('Apply AI fix to all issues on this post', 'seoforge'); ?>"
                            style="position:absolute;right:0;top:4px;z-index:2;<?php echo count($fixable_issues) > 1 ? '' : 'display:none;'; ?>">
                        <?php /* translators: %d: number of fixable SEO issues */ ?>
                        <?php printf(esc_html__('Fix all %d', 'seoforge'), count($fixable_issues)); ?>
                        <span class="sf-credits-badge"><svg viewBox="0 0 24 22" fill="currentColor" aria-hidden="true"><polygon points="7,4 17,4 21,9 12,19 3,9"/></svg> <?php echo esc_html( $total_cost ); ?></span>
                    </button>
                <?php endif; ?>
                <details class="seoforge-issues" style="margin-top:18px;<?php echo $issues_total ? '' : 'display:none;'; ?>" <?php echo ($issues_total && $issues_total <= 6) ? 'open' : ''; ?>>
                    <summary style="cursor:pointer;font-weight:600;font-size:14px;padding:6px 70px 6px 0;user-select:none;">
                        <?php esc_html_e('Issues', 'seoforge'); ?> <span style="background:#ef4444;color:#fff;font-size:14px;padding:1px 7px;border-radius:10px;font-weight:600;margin-left:4px;"><?php echo esc_html( $issues_total ); ?></span><?php if (class_exists('Forge_Docs_Link')) Forge_Docs_Link::render('seoforge', 'fixing-seo-issues-with-ai-pro', __('SEO issues', 'seoforge'), __('Problems the analyzer flagged. Click Fix to let AI rewrite the offending part.', 'seoforge')); ?>
                    </summary>
                    <ul>
                    <?php
                    // Fix buttons appear only for issues our AI endpoints can
                    // resolve (shared helper keeps this in sync with the AJAX
                    // path). SF-QA-001: Free users (no $ai_pro) never see Fix.
                    $is_ai_fixable = $ai_pro
                        ? function ($msg) use ($has_editable) { return SEOFORGE_Analyzer::is_issue_ai_fixable($msg, $has_editable); }
                        : function ($msg) { return false; };
                    ?>
                    <?php
                    $sf_credit_svg = '<svg viewBox="0 0 24 22" fill="currentColor" aria-hidden="true"><polygon points="7,4 17,4 21,9 12,19 3,9"/></svg>';
                    $expand_cost = (class_exists('Forge_Credits_Widget') && method_exists('Forge_Credits_Widget', 'get_action_cost'))
                        ? (int) Forge_Credits_Widget::get_action_cost('fix_issue') : 2;
                    $mb_heading_count = preg_match_all('/<h[2-4][^>]*>/i', (string) $analyzer->resolve_content_html($post));
                    ?>
                    <?php foreach ($problems as $issue):
                        $imsg = $issue['message'];
                        $fixable = $is_ai_fixable($imsg);
                        $issue_cost = SEOFORGE_Analyzer::fix_credit_cost($imsg);
                        // content-body rewrites open the review drawer
                        // (Before/After) instead of applying in place.
                        $content_fix = $ai_pro && $has_editable && SEOFORGE_Analyzer::is_issue_content_rewrite($imsg, $has_editable);
                        $expandable = $ai_pro && $has_editable && SEOFORGE_Analyzer::is_issue_expandable($imsg);
                        $toc_ok      = $ai_pro && $has_editable && SEOFORGE_Analyzer::is_issue_toc($imsg) && $mb_heading_count >= 2;
                        $external_ok = $ai_pro && $has_editable && SEOFORGE_Analyzer::is_issue_external($imsg);
                        $subhead_ok  = $ai_pro && $has_editable && SEOFORGE_Analyzer::is_issue_subheading_keyword($imsg) && $mb_heading_count >= 1;
                    ?>
                        <li class="seoforge-issue seoforge-issue--<?php echo esc_attr($issue['severity'] ?? 'warning'); ?>">
                            <span class="sf-issue-text"><?php echo esc_html($imsg); ?></span>
                            <?php
                            // Per-issue "?" help tooltip + docs link (empty when no code/help).
                            echo SEOFORGE_Analyzer::issue_help_tooltip($issue['code'] ?? '', $imsg); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper
                            ?>
                            <?php if ($content_fix): ?>
                            <button type="button" class="sf-content-fix-btn" data-post="<?php echo esc_attr( $post->ID ); ?>" data-issue="<?php echo esc_attr($imsg); ?>" data-issue-type="<?php echo esc_attr($issue['code'] ?? ''); ?>" title="<?php esc_attr_e('Preview the change before applying', 'seoforge'); ?>"><?php esc_html_e('Fix', 'seoforge'); ?><?php if ($issue_cost > 0): ?> <span class="sf-credits-badge"><?php echo $sf_credit_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built SVG markup ?> <?php echo esc_html( $issue_cost ); ?></span><?php endif; ?></button>
                            <?php elseif ($ai_pro && $fixable): ?>
                            <button type="button" class="sf-fix-btn" data-post="<?php echo esc_attr( $post->ID ); ?>" data-issue="<?php echo esc_attr($imsg); ?>" title="<?php esc_attr_e('Spends AI credits', 'seoforge'); ?>"><?php esc_html_e('Fix', 'seoforge'); ?> <span class="sf-credits-badge"><?php echo $sf_credit_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built SVG markup ?> <?php echo esc_html( $issue_cost ); ?></span></button>
                            <?php elseif ($expandable): ?>
                            <button type="button" class="sf-expand-btn" data-post="<?php echo esc_attr( $post->ID ); ?>" data-issue="<?php echo esc_attr($imsg); ?>" title="<?php esc_attr_e('Preview an AI expansion before applying', 'seoforge'); ?>"><?php esc_html_e('Expand with AI', 'seoforge'); ?> <span class="sf-credits-badge"><?php echo $sf_credit_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built SVG markup ?> <?php echo esc_html( $expand_cost ); ?></span></button>
                            <?php elseif ($toc_ok): ?>
                            <button type="button" class="sf-toc-btn" data-post="<?php echo esc_attr( $post->ID ); ?>" data-issue="<?php echo esc_attr($imsg); ?>" title="<?php esc_attr_e('Build a table of contents from your headings', 'seoforge'); ?>"><?php esc_html_e('Insert ToC', 'seoforge'); ?></button>
                            <?php elseif ($external_ok): ?>
                            <button type="button" class="sf-ext-btn" data-post="<?php echo esc_attr( $post->ID ); ?>" data-issue="<?php echo esc_attr($imsg); ?>" title="<?php esc_attr_e('Preview AI external-link suggestions before applying', 'seoforge'); ?>"><?php esc_html_e('Suggest links', 'seoforge'); ?> <span class="sf-credits-badge"><?php echo $sf_credit_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built SVG markup ?> <?php echo esc_html( $expand_cost ); ?></span></button>
                            <?php elseif ($subhead_ok): ?>
                            <button type="button" class="sf-subhead-btn" data-post="<?php echo esc_attr( $post->ID ); ?>" data-issue="<?php echo esc_attr($imsg); ?>" title="<?php esc_attr_e('Rewrite one subheading to include the focus keyword (reviewed before saving)', 'seoforge'); ?>"><?php esc_html_e('Add to subheading', 'seoforge'); ?> <span class="sf-credits-badge"><?php echo $sf_credit_svg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built SVG markup ?> <?php echo esc_html( $expand_cost ); ?></span></button>
                            <?php endif; ?>
                        </li>
                    <?php endforeach; ?>
                    </ul>
                </details>
                <?php // optional click-through ideas — never counted as problems. ?>
                <details class="seoforge-issues seoforge-ideas" style="margin-top:12px;<?php echo $ideas ? '' : 'display:none;'; ?>" <?php echo $ideas ? 'open' : ''; ?>>
                    <summary style="cursor:pointer;font-weight:600;font-size:14px;padding:6px 0;user-select:none;color:#475569;">
                        <?php esc_html_e('Optimization ideas', 'seoforge'); ?> <span style="background:#e2e8f0;color:#475569;font-size:13px;padding:1px 7px;border-radius:10px;font-weight:600;margin-left:4px;"><?php echo esc_html(count($ideas)); ?></span><?php if (class_exists('Forge_Docs_Link')) Forge_Docs_Link::render('seoforge', 'writing-seo-titles-and-descriptions', __('Optimization ideas', 'seoforge'), __('Optional tweaks that can lift click-through. Not required for Google rankings.', 'seoforge')); ?>
                    </summary>
                    <ul>
                    <?php foreach ($ideas as $issue):
                        $imsg = $issue['message'];
                        $fixable = $ai_pro ? SEOFORGE_Analyzer::is_issue_ai_fixable($imsg, $has_editable) : false;
                        $issue_cost = SEOFORGE_Analyzer::fix_credit_cost($imsg);
                    ?>
                        <li class="seoforge-issue seoforge-issue--info seoforge-issue--idea">
                            <span class="sf-issue-text"><?php echo esc_html($imsg); ?></span>
                            <?php echo SEOFORGE_Analyzer::issue_help_tooltip($issue['code'] ?? '', $imsg); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                            <?php if ($ai_pro && $fixable): ?>
                            <button type="button" class="sf-fix-btn" data-post="<?php echo esc_attr( $post->ID ); ?>" data-issue="<?php echo esc_attr($imsg); ?>" title="<?php esc_attr_e('Spends AI credits', 'seoforge'); ?>"><?php esc_html_e('Apply', 'seoforge'); ?> <span class="sf-credits-badge"><svg viewBox="0 0 24 22" fill="currentColor" aria-hidden="true"><polygon points="7,4 17,4 21,9 12,19 3,9"/></svg> <?php echo esc_html( $issue_cost ); ?></span></button>
                            <?php endif; ?>
                        </li>
                    <?php endforeach; ?>
                    </ul>
                </details>
                </div><!-- /issues wrapper -->
                <?php /* issues container always rendered (see note above) */ ?>
            </div><!-- /.sf-mb-panel SEO -->

            <!-- ========== TAB: Social ========== -->
            <div class="sf-mb-panel" data-sf-mbpanel="social" id="sf-mb-panel-social" role="tabpanel" aria-labelledby="sf-mb-tab-social" hidden>
                <?php // F14 2026-04-21: Social tab body extracted to class-seo-preview.php
                SEOFORGE_Preview_View::render_social([
                    'post'           => $post,
                    'meta_title'     => $meta_title,
                    'meta_desc'      => $meta_desc,
                    'og_title'       => $og_title,
                    'og_description' => $og_description,
                    'og_image_id'    => $og_image_id,
                    'og_image_url'   => $og_image_url,
                    'twitter_title'  => $twitter_title,
                    'twitter_desc'   => $twitter_desc,
                    'thumb_url'      => $thumb_url,
                ]);
                ?>
            </div><!-- /.sf-mb-panel Social -->

            <!-- ========== TAB: Schema ========== -->
            <div class="sf-mb-panel" data-sf-mbpanel="schema" id="sf-mb-panel-schema" role="tabpanel" aria-labelledby="sf-mb-tab-schema" hidden>
                <!-- Schema Builder (primary content of the Schema tab) -->
                <?php $analyzer->render_schema_builder($post); ?>
            </div><!-- /.sf-mb-panel Schema -->

            <!-- ========== TAB: Advanced ========== -->
            <div class="sf-mb-panel" data-sf-mbpanel="advanced" id="sf-mb-panel-advanced" role="tabpanel" aria-labelledby="sf-mb-tab-advanced" hidden>

                <!-- Search visibility (noindex/nofollow) -->
                <div class="sf-mb-section">
                    <div class="sf-mb-section__head">
                        <span class="dashicons dashicons-visibility"></span>
                        <strong><?php esc_html_e('Search Visibility', 'seoforge'); ?><?php if (class_exists('Forge_Docs_Link')) Forge_Docs_Link::render('seoforge', 'hiding-pages-from-search-engines-noindex', __('Search visibility', 'seoforge'), __('noindex hides the page from Google; nofollow stops link equity from flowing out.', 'seoforge')); ?></strong>
                        <?php if ($noindex): ?><span class="sf-mb-tab__badge sf-mb-tab__badge--warn" style="margin-left:auto;">noindex</span><?php endif; ?>
                    </div>
                    <div class="sf-mb-section__body">
                        <label style="display:flex;align-items:center;gap:8px;font-size:14px;margin-bottom:8px;cursor:pointer;">
                            <input type="checkbox" name="seoforge_noindex" value="1" <?php checked($noindex); ?>>
                            <span><?php esc_html_e('Hide from search results', 'seoforge'); ?> <small style="color:#94a3b8;">(noindex)</small></span>
                        </label>
                        <label style="display:flex;align-items:center;gap:8px;font-size:14px;cursor:pointer;">
                            <input type="checkbox" name="seoforge_nofollow" value="1" <?php checked($nofollow); ?>>
                            <span><?php esc_html_e('Do not follow links on this page', 'seoforge'); ?> <small style="color:#94a3b8;">(nofollow)</small></span>
                        </label>
                    </div>
                </div>

                <!-- Canonical URL -->
                <div class="sf-mb-section">
                    <div class="sf-mb-section__head">
                        <span class="dashicons dashicons-admin-links"></span>
                        <strong><?php esc_html_e('Canonical URL', 'seoforge'); ?><?php if (class_exists('Forge_Docs_Link')) Forge_Docs_Link::render('seoforge', 'custom-canonical-url', __('Canonical URL', 'seoforge'), __('Use only when this post should point search engines to another preferred URL.', 'seoforge')); ?></strong>
                    </div>
                    <div class="sf-mb-section__body">
                        <input type="url" name="seoforge_canonical_url" value="<?php echo esc_attr($canonical_url); ?>" class="widefat" placeholder="<?php echo esc_attr(get_permalink($post)); ?>">
                        <p style="margin:8px 0 0;font-size:13px;color:#64748b;"><?php esc_html_e('Leave empty to use the normal post permalink.', 'seoforge'); ?></p>
                    </div>
                </div>

                <?php if (SEOFORGE_License::instance()->can('gsc')): ?>
                <!-- Search Engine Submission (Google Indexing API) — merged from the
 old standalone metabox so it shares the plugin styling. -->
                <div class="sf-mb-section">
                    <div class="sf-mb-section__head">
                        <span class="dashicons dashicons-google"></span>
                        <strong><?php esc_html_e('Search Engine Submission', 'seoforge'); ?></strong>
                    </div>
                    <div class="sf-mb-section__body">
                        <?php SEOFORGE_Indexing::instance()->render_inline($post); ?>
                    </div>
                </div>
                <?php endif; ?>

                <?php if ($ai_pro): ?>
                <!-- AI tools row -->
                <div class="sf-mb-section">
                    <div class="sf-mb-section__head">
                        <span class="dashicons dashicons-superhero-alt"></span>
                        <strong><?php esc_html_e('AI Tools', 'seoforge'); ?><?php if (class_exists('Forge_Docs_Link')) Forge_Docs_Link::render('seoforge', 'using-ai-to-write-titles-and-descriptions-pro', __('AI tools', 'seoforge'), __('Auto-link related posts, check AI readiness, and rewrite body content. Costs credits.', 'seoforge')); ?></strong>
                    </div>
                    <div class="sf-mb-section__body">
                        <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
                            <button type="button" class="sf-action-btn" id="sf-auto-link" data-post="<?php echo esc_attr( $post->ID ); ?>">
                                <?php esc_html_e('Auto-Link', 'seoforge'); ?> <?php echo Forge_Credits_Widget::render_badge_html('auto_insert_links', 'sf'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                            </button>
                            <button type="button" class="sf-action-btn" id="sf-ai-readiness" data-post="<?php echo esc_attr( $post->ID ); ?>">
                                AI Readiness <?php echo Forge_Credits_Widget::render_badge_html('analyze_readiness', 'sf'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                            </button>
                        </div>
                        <div id="sf-ai-readiness-result" style="display:none;margin-bottom:10px;"></div>
                        <button type="button" class="sf-optimize-btn" id="sf-optimize-content" data-post="<?php echo esc_attr( $post->ID ); ?>">
                            <?php esc_html_e('Optimize content', 'seoforge'); ?> <?php echo Forge_Credits_Widget::render_badge_html('optimize_content', 'sf'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                        </button>
                        <div id="sf-optimizer-result" style="display:none;margin-top:10px;"></div>
                    </div>
                </div>
                <?php elseif ($ai_locked): ?>
                <!-- locked AI Tools (Free) — visible AI Content Analysis
 upsell. Buttons carry no REST wiring; a click opens the
 paywall modal instead of consuming credits. -->
                <div class="sf-mb-section">
                    <div class="sf-mb-section__head">
                        <span class="dashicons dashicons-superhero-alt"></span>
                        <strong><?php esc_html_e('AI Tools', 'seoforge'); ?></strong>
                        <span class="sf-mb-tab__badge sf-mb-tab__badge--pro" style="margin-left:auto;">PRO</span>
                    </div>
                    <div class="sf-mb-section__body">
                        <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">
                            <?php echo SEOFORGE_License::ai_lock_html('autolink', __('Auto-Link', 'seoforge'), 'action'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                            <?php echo SEOFORGE_License::ai_lock_html('readiness', __('AI Content Analysis', 'seoforge'), 'action'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                        </div>
                        <?php echo SEOFORGE_License::ai_lock_html('optimize', __('Optimize content', 'seoforge'), 'block'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted pre-built markup from render helper ?>
                    </div>
                </div>
                <?php endif; ?>

                <!-- Video SEO -->
                <div class="sf-mb-section">
                    <div class="sf-mb-section__head">
                        <span class="dashicons dashicons-video-alt3"></span>
                        <strong><?php esc_html_e('Video SEO', 'seoforge'); ?><?php if (class_exists('Forge_Docs_Link')) Forge_Docs_Link::render('seoforge', 'video-seo-pro', __('Video SEO', 'seoforge'), __('VideoObject schema for YouTube/Vimeo/HTML5 embeds so Google can show rich video results.', 'seoforge')); ?></strong>
                        <?php if (!SEOFORGE_License::instance()->can('video_seo')): ?>
                            <span class="sf-mb-tab__badge sf-mb-tab__badge--pro" style="margin-left:auto;">PRO</span>
                        <?php endif; ?>
                    </div>
                    <div class="sf-mb-section__body">
                        <?php if (SEOFORGE_License::instance()->can('video_seo')): ?>
                            <?php SEOFORGE_Video_Seo::instance()->render_metabox_fields($post); ?>
                        <?php else: ?>
                            <p style="margin:0;font-size:14px;color:#64748b;"><?php esc_html_e('Add video metadata for better video search visibility — available on Pro.', 'seoforge'); ?></p>
                        <?php endif; ?>
                    </div>
                </div>

                <!-- A/B Testing (bottom of Advanced tab) -->
                <?php if (class_exists('SEOFORGE_AB_Testing')): ?>
                    <div style="margin-top:16px;">
                        <?php SEOFORGE_AB_Testing::instance()->render_inline($post); ?>
                    </div>
                <?php endif; ?>
            </div><!-- /.sf-mb-panel Advanced -->

            <?php wp_nonce_field('seoforge_save_meta', 'seoforge_meta_nonce'); ?>
        </div>

        <style>
        /* follow-up: design-system checkboxes inside the SEO metabox.
 The body on a post-edit screen doesn't carry .sf-admin, so the
 generic admin.css checkbox rules don't reach this metabox — we
 scope them here against .seoforge-metabox. Same visual as the rest
 of the plugin admin UI: 18×18 rounded, brand-orange fill, white
 checkmark, focus ring. */
        /* Boost specificity with body.wp-admin so the rule beats any rule-set
 that already targets input[type=checkbox] at admin-page level. */
        body.wp-admin .seoforge-metabox input[type="checkbox"]{
            appearance:none !important; -webkit-appearance:none !important;
            width:18px !important; height:18px !important; min-width:18px;
            margin:0 !important; padding:0 !important;
            border:1.5px solid #cbd5e1 !important; border-radius:5px !important;
            background-color:#fff !important; cursor:pointer; position:relative;
            transition:border-color 0.15s, background-color 0.15s, box-shadow 0.15s;
            vertical-align:middle; flex-shrink:0;
        }
        body.wp-admin .seoforge-metabox input[type="checkbox"]:hover{ border-color:#FF6B35 !important; }
        body.wp-admin .seoforge-metabox input[type="checkbox"]:focus-visible{ outline:none !important; box-shadow:0 0 0 3px rgba(255,107,53,0.25) !important; }
        body.wp-admin .seoforge-metabox input[type="checkbox"]::before{ display:none !important; content:none !important; }
        body.wp-admin .seoforge-metabox input[type="checkbox"]:checked{ background-color:#FF6B35 !important; border-color:#FF6B35 !important; }
        body.wp-admin .seoforge-metabox input[type="checkbox"]:checked::after{
            content:'' !important; display:block !important; position:absolute !important;
            left:5px; top:1px; width:5px; height:10px;
            border:solid #fff !important; border-width:0 2px 2px 0 !important;
            transform:rotate(45deg); background:transparent !important;
        }
        /* follow-up: Per-design-system Select image button (matches the
 orange Settings button instead of the WordPress default .button) */
        .seoforge-metabox .sf-og-image-btn{display:inline-flex;align-items:center;gap:6px;background:#FF6B35;color:#fff;border:none;border-radius:8px;padding:9px 18px;font-size:14px;font-weight:600;cursor:pointer;box-shadow:0 1px 3px rgba(255,107,53,0.18);transition:all 0.15s;}
        .seoforge-metabox .sf-og-image-btn:hover{background:#F05E28;transform:translateY(-1px);box-shadow:0 4px 12px rgba(255,107,53,0.30);}
        .seoforge-metabox .sf-og-image-btn .dashicons{font-size:15px;width:15px;height:15px;}
        .seoforge-metabox .sf-og-image-remove{background:transparent;border:1px solid #fecaca;color:#dc2626;border-radius:8px;padding:8px 14px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.15s;}
        .seoforge-metabox .sf-og-image-remove:hover{background:#fef2f2;border-color:#dc2626;}

        /* Metabox horizontal tabs */
        .seoforge-metabox .sf-mb-tabs{display:flex;gap:2px;margin:14px 0 0;padding:4px;background:#f1f5f9;border-radius:10px;overflow-x:auto;}
        .seoforge-metabox .sf-mb-tab{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;background:transparent;border:none;color:#475569;font-size:14px;font-weight:600;padding:9px 14px;border-radius:7px;cursor:pointer;white-space:nowrap;transition:all 0.15s;position:relative;}
        .seoforge-metabox .sf-mb-tab .dashicons{font-size:15px;width:15px;height:15px;}
        .seoforge-metabox .sf-mb-tab:hover{color:#1e293b;background:rgba(255,255,255,0.6);}
        .seoforge-metabox .sf-mb-tab.is-active{background:#fff;color:#E05520;box-shadow:0 1px 3px rgba(0,0,0,0.08);}
        .seoforge-metabox .sf-mb-tab:focus-visible{outline:2px solid #FF6B35;outline-offset:2px;}
        .seoforge-metabox .sf-mb-tab__badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 6px;background:#ef4444;color:#fff;border-radius:9px;font-size:14px;font-weight:700;margin-left:2px;}
        .seoforge-metabox .sf-mb-tab__badge--warn{background:#f59e0b;}
        .seoforge-metabox .sf-mb-tab__badge--pro{background:linear-gradient(135deg,#FF6B35,#E05520);}
        .seoforge-metabox .sf-mb-tab__badge--issues{background:#ef4444;}
        .seoforge-metabox .sf-mb-panel{padding:18px 4px 4px;animation:sfMbFade .18s ease-out;}
        .seoforge-metabox .sf-mb-panel[hidden]{display:none;}
        @keyframes sfMbFade{from{opacity:0;transform:translateY(2px);}to{opacity:1;transform:none;}}
        /* Section blocks inside Advanced/Schema panels */
        .seoforge-metabox .sf-mb-section{margin-bottom:14px;border:1px solid #e2e8f0;border-radius:10px;background:#fff;overflow:hidden;}
        .seoforge-metabox .sf-mb-section:last-child{margin-bottom:0;}
        .seoforge-metabox .sf-mb-section__head{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#f8fafc;border-bottom:1px solid #e2e8f0;font-size:14px;color:#1e293b;}
        .seoforge-metabox .sf-mb-section__head .dashicons{color:#FF6B35;font-size:16px;width:16px;height:16px;}
        .seoforge-metabox .sf-mb-section__body{padding:14px;}
        @media(max-width:782px){
            .seoforge-metabox .sf-mb-tab{font-size:14px;padding:8px 10px;}
            .seoforge-metabox .sf-mb-tab .dashicons{display:none;}
        }

        .seoforge-metabox .sf-field-group{margin:0 0 16px 0!important;padding:0!important;clear:both;overflow:hidden;}
        .seoforge-metabox .sf-field-header{display:flex!important;justify-content:space-between!important;align-items:center!important;margin:0 0 5px 0!important;}
        .seoforge-metabox .sf-field-header strong{font-size:14px!important;font-weight:600!important;color:#1e293b!important;}
        .seoforge-metabox input[type=text],.seoforge-metabox input[type=url],.seoforge-metabox textarea,.seoforge-metabox select{
            border:1px solid #e2e8f0!important;border-radius:6px!important;padding:8px 10px!important;font-size:14px!important;width:100%!important;box-sizing:border-box!important;
            transition:border-color 0.15s!important;margin:0!important;
        }
        .seoforge-metabox input[type=text]:focus,.seoforge-metabox input[type=url]:focus,.seoforge-metabox textarea:focus{border-color:#FF6B35!important;outline:none!important;box-shadow:0 0 0 3px rgba(255,107,53,0.1)!important;}
        .seoforge-metabox .seoforge-char-count{display:block!important;text-align:right!important;margin:2px 0 0 0!important;float:none!important;clear:both!important;}
        .sf-ai-badge{display:inline-flex;align-items:center;gap:4px;background:linear-gradient(135deg,#FF6B35,#E05520);color:#fff;padding:3px 10px;border-radius:12px;font-size:14px;font-weight:700;margin-bottom:10px;}
        .sf-ai-badge .dashicons{font-size:14px;width:12px;height:12px;}
        .sf-ai-gen-btn{background:linear-gradient(135deg,#FF6B35,#E05520);color:#fff;border:none;border-radius:4px;padding:2px 8px;font-size:14px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:3px;transition:opacity 0.15s;}
        .sf-ai-gen-btn:hover{opacity:0.85;}
        .sf-ai-gen-btn .dashicons{font-size:14px;width:12px;height:12px;}
        .sf-ai-gen-btn.loading{opacity:0.5;pointer-events:none;}
        .sf-ai-gen-both{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;background:linear-gradient(135deg,#FF6B35,#E05520);color:#fff;border:none;border-radius:6px;padding:8px;font-size:14px;font-weight:600;cursor:pointer;margin:8px 0;transition:opacity 0.15s;}
        .sf-ai-gen-both:hover{opacity:0.9;}
        .sf-ai-gen-both .dashicons{font-size:14px;width:14px;height:14px;}
        .sf-ai-gen-both.loading{opacity:0.5;pointer-events:none;}
        /* Fix button on issues */
        .seoforge-issue{display:flex;align-items:center;justify-content:space-between;gap:6px;}
        .sf-issue-text{flex:1;}
        .sf-fix-btn{background:linear-gradient(135deg,#FF6B35,#E05520);color:#fff;border:none;border-radius:4px;padding:2px 8px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap;transition:opacity 0.15s;display:inline-flex;align-items:center;gap:4px;}
        .sf-fix-btn:hover{opacity:0.85;}
        /* Review-drawer actions (Expand / Insert ToC / Suggest links): outlined
 to signal "opens a review", not an instant change. */
        .sf-expand-btn,.sf-toc-btn,.sf-ext-btn,.sf-subhead-btn,.sf-content-fix-btn{background:#fff;color:#E05520;border:1px solid #FF6B35;border-radius:4px;padding:2px 8px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap;transition:all 0.15s;display:inline-flex;align-items:center;gap:4px;}
        .sf-expand-btn:hover,.sf-toc-btn:hover,.sf-ext-btn:hover,.sf-subhead-btn:hover,.sf-content-fix-btn:hover{background:#FFF7ED;}
        .sf-expand-btn .sf-credits-badge,.sf-ext-btn .sf-credits-badge,.sf-subhead-btn .sf-credits-badge,.sf-content-fix-btn .sf-credits-badge{background:rgba(255,107,53,0.12);color:#E05520;}
        .sf-fix-btn.loading{opacity:0.7;pointer-events:none;}
        .sf-fix-btn.loading::before{
            content:"";
            display:inline-block;width:10px;height:10px;
            border:2px solid rgba(255,255,255,0.35);
            border-top-color:#fff;
            border-radius:50%;
            animation:sf-spin 0.7s linear infinite;
            margin-right:4px;
        }
        @keyframes sf-spin{to{transform:rotate(360deg);}}
        /* Credits badge — icon + number, icon inherits label color via
 currentColor on the SVG fill. */
        .sf-credits-badge{display:inline-flex;align-items:center;gap:3px;background:rgba(0,0,0,0.15);color:inherit;font-size:14px;font-weight:600;padding:1px 6px 1px 5px;border-radius:3px;vertical-align:middle;margin-left:2px;letter-spacing:0.3px;}
        .sf-credits-badge svg{width:11px;height:11px;fill:currentColor;flex-shrink:0;}
        .sf-ai-gen-btn .sf-credits-badge,
        .sf-fix-btn .sf-credits-badge,
        .sf-ai-gen-both .sf-credits-badge{background:rgba(255,255,255,0.25);color:#fff;}
        .sf-action-btn .sf-credits-badge{background:rgba(255,107,53,0.12);color:#FF6B35;}
        /* Action buttons */
        .sf-action-btn{display:inline-flex;align-items:center;gap:4px;background:#fff;border:1px solid #FF6B35;color:#FF6B35;border-radius:5px;padding:4px 10px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.15s;}
        .sf-action-btn:hover{background:#FFF7ED;}
        .sf-action-btn .dashicons{font-size:14px;width:13px;height:13px;}
        .sf-action-btn.loading{opacity:0.5;pointer-events:none;}
        /* AI Readiness result */
        .sf-aeo-score{display:flex;align-items:center;gap:10px;padding:10px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;margin-bottom:8px;}
        .sf-aeo-score.low{background:#fef2f2;border-color:#fecaca;}
        .sf-aeo-score.mid{background:#fffbeb;border-color:#fde68a;}
        .sf-aeo-num{font-size:24px;font-weight:800;line-height:1;}
        .sf-aeo-checks{margin-top:6px;font-size:14px;}
        .sf-aeo-check{padding:2px 0;display:flex;align-items:center;gap:4px;}
        .sf-aeo-check.pass{color:#059669;}.sf-aeo-check.fail{color:#dc2626;}
        /* Content Optimizer */
        .sf-optimize-btn{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;background:linear-gradient(135deg,#FF6B35,#E05520);color:#fff;border:none;border-radius:6px;padding:10px;font-size:14px;font-weight:600;cursor:pointer;margin:10px 0 0;transition:opacity 0.15s;box-shadow:0 1px 3px rgba(255,107,53,0.3);}
        .sf-optimize-btn:hover{opacity:0.9;}
        .sf-optimize-btn .dashicons{font-size:15px;width:15px;height:15px;}
        .sf-optimize-btn.loading{opacity:0.6;pointer-events:none;}
        .sf-opt-score-bar{display:flex;align-items:center;gap:10px;padding:10px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;margin-bottom:10px;}
        .sf-opt-score-num{font-size:22px;font-weight:800;line-height:1;min-width:36px;text-align:center;}
        .sf-opt-card{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:10px;margin-bottom:8px;transition:border-color 0.15s;}
        .sf-opt-card:hover{border-color:#FED7AA;}
        .sf-opt-card-header{display:flex;align-items:center;gap:6px;margin-bottom:6px;}
        .sf-opt-priority{display:inline-flex;align-items:center;justify-content:center;width:24px;height:20px;border-radius:4px;font-size:14px;font-weight:700;color:#fff;flex-shrink:0;}
        .sf-opt-priority.p1{background:#dc2626;}.sf-opt-priority.p2{background:#d97706;}.sf-opt-priority.p3{background:#6b7280;}
        .sf-opt-card-title{font-size:14px;font-weight:700;color:#1e293b;flex:1;}
        .sf-opt-card-desc{font-size:14px;color:#64748b;line-height:1.5;margin-bottom:6px;}
        .sf-opt-card-example{display:none;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:6px;padding:8px;margin-top:6px;font-size:14px;color:#334155;line-height:1.5;max-height:200px;overflow-y:auto;}
        .sf-opt-card-example.open{display:block;}
        .sf-opt-card-actions{display:flex;gap:4px;margin-top:6px;}
        .sf-opt-toggle-example{background:none;border:1px solid #cbd5e1;color:#64748b;border-radius:4px;padding:2px 8px;font-size:14px;font-weight:600;cursor:pointer;transition:all 0.15s;}
        .sf-opt-toggle-example:hover{border-color:#FF6B35;color:#FF6B35;}
        .sf-opt-apply-btn{background:linear-gradient(135deg,#059669,#10b981);color:#fff;border:none;border-radius:4px;padding:2px 10px;font-size:14px;font-weight:700;cursor:pointer;transition:opacity 0.15s;}
        .sf-opt-apply-btn:hover{opacity:0.85;}
        .sf-opt-apply-btn.loading{opacity:0.5;pointer-events:none;}
        .sf-opt-type-icon{font-size:14px;width:12px;height:12px;color:#FF6B35;}

        /* ─────────── Wide metabox polish (normal context, follow-up) ─────────── */
        #seoforge_seo_box > .inside { padding: 0 !important; margin: 0 !important; }
        #seoforge_seo_box { border-radius: 10px !important; box-shadow: 0 1px 3px rgba(0,0,0,0.04) !important; }
        #seoforge_seo_box > .postbox-header h2 { font-size: 15px !important; font-weight: 700 !important; color: #1e293b !important; }

        .seoforge-metabox--wide { padding: 22px 26px 24px !important; font-size: 14px !important; max-width: 1280px; margin: 0 auto !important; }
        .seoforge-metabox--wide .sf-mb-header { display: flex; align-items: center; justify-content: center; gap: 18px; padding-bottom: 18px; margin-bottom: 22px; border-bottom: 1px solid #e2e8f0; }
        .seoforge-metabox--wide .sf-mb-header .seoforge-score-circle {
            position: static !important; float: none !important; margin: 0 !important;
            width: 68px !important; height: 68px !important; border-width: 4px !important;
            display: flex !important; align-items: center !important; justify-content: center !important;
            font-size: 26px !important; font-weight: 800 !important; line-height: 1 !important;
        }
        .seoforge-metabox--wide .sf-mb-header-title { font-size: 20px; font-weight: 700; color: #0f172a; letter-spacing: -0.2px; }
        .seoforge-metabox--wide .sf-mb-header-sub { font-size: 14px; color: #64748b; margin-top: 3px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
        .sf-ai-badge-inline { display: inline-flex; align-items: center; gap: 4px; background: linear-gradient(135deg,#FF6B35,#E05520); color: #fff; padding: 2px 8px; border-radius: 10px; font-size: 14px; font-weight: 700; }
        .sf-ai-badge-inline .dashicons { font-size: 14px; width: 12px; height: 12px; }

        .seoforge-metabox--wide .sf-mb-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; align-items: start; }
        @media (max-width: 1100px) { .seoforge-metabox--wide .sf-mb-grid { grid-template-columns: 1fr; gap: 22px; } }

        .seoforge-metabox--wide .sf-field-group { margin: 0 0 18px 0 !important; }
        .seoforge-metabox--wide .sf-field-header strong { font-size: 14px !important; color: #475569 !important; text-transform: uppercase; letter-spacing: 0.4px; }
        .seoforge-metabox--wide input[type=text],
        .seoforge-metabox--wide input[type=url],
        .seoforge-metabox--wide textarea,
        .seoforge-metabox--wide select {
            font-size: 15px !important; padding: 11px 14px !important;
            border: 1px solid #d1d5db !important; border-radius: 8px !important; line-height: 1.5 !important;
        }
        .seoforge-metabox--wide textarea { min-height: 90px !important; }
        .seoforge-metabox--wide .seoforge-char-count { font-size: 14px !important; color: #94a3b8 !important; padding-top: 4px !important; }

        .seoforge-metabox--wide .sf-ai-gen-btn { font-size: 14px !important; padding: 4px 10px !important; border-radius: 6px !important; }
        .seoforge-metabox--wide .sf-ai-gen-both { font-size: 14px !important; padding: 11px !important; border-radius: 8px !important; }
        .seoforge-metabox--wide .sf-action-btn { font-size: 14px !important; padding: 7px 14px !important; border-radius: 7px !important; }
        .seoforge-metabox--wide .sf-action-btn .dashicons { font-size: 15px; width: 15px; height: 15px; }
        .seoforge-metabox--wide .sf-optimize-btn { font-size: 14px !important; padding: 12px !important; border-radius: 8px !important; }

        .seoforge-metabox--wide .seoforge-serp-preview { padding: 14px 16px !important; border: 1px solid #e2e8f0 !important; border-radius: 10px !important; background: #fff !important; }
        .seoforge-metabox--wide .seoforge-serp-title { font-size: 18px !important; line-height: 1.35 !important; color: #1a0dab !important; }
        .seoforge-metabox--wide .seoforge-serp-url { font-size: 14px !important; color: #1e8e3e !important; margin: 2px 0 6px !important; }
        .seoforge-metabox--wide .seoforge-serp-desc { font-size: 14px !important; line-height: 1.55 !important; color: #4d5156 !important; }

        .seoforge-metabox--wide .seoforge-issues summary { font-size: 14px !important; padding: 10px 0 !important; }
        .seoforge-metabox--wide .seoforge-issue { font-size: 14px; padding: 8px 12px !important; }
        .seoforge-metabox--wide .sf-fix-btn { font-size: 14px !important; padding: 4px 12px !important; border-radius: 6px !important; }
        </style>
        <script>
 // Metabox tab switcher.
        // - Active tab persists per post type via localStorage.
        // - ArrowLeft/Right + Home/End for keyboard nav (ARIA tablist).
        // - Hidden via [hidden] attr (not display:none) so semantics + tab order
        //   are correct for assistive tech.
        (function(){
            var box = document.getElementById('seoforge_seo_box');
            if (!box) return;
            var KEY = 'sf_mbtab_v1_<?php echo esc_js(get_post_type($post)); ?>';
            var DEFAULT_TAB = 'seo';
            var tabs   = box.querySelectorAll('.sf-mb-tab');
            var panels = box.querySelectorAll('.sf-mb-panel');
            if (!tabs.length || !panels.length) return;
            var ids = Array.prototype.map.call(tabs, function(t){ return t.getAttribute('data-sf-mbtab'); });

            function activate(id, opts){
                opts = opts || {};
                if (ids.indexOf(id) === -1) id = DEFAULT_TAB;
                Array.prototype.forEach.call(tabs, function(t){
                    var on = t.getAttribute('data-sf-mbtab') === id;
                    t.classList.toggle('is-active', on);
                    t.setAttribute('aria-selected', on ? 'true' : 'false');
                    t.tabIndex = on ? 0 : -1;
                });
                Array.prototype.forEach.call(panels, function(p){
                    var on = p.getAttribute('data-sf-mbpanel') === id;
                    if (on) p.removeAttribute('hidden'); else p.setAttribute('hidden','');
                });
                if (!opts.silent) { try { localStorage.setItem(KEY, id); } catch(e){} }
            }

            // Pick initial tab from localStorage.
            var saved = DEFAULT_TAB;
            try { var s = localStorage.getItem(KEY); if (s && ids.indexOf(s) !== -1) saved = s; } catch(e){}
            activate(saved, {silent:true});

            // Click handlers.
            Array.prototype.forEach.call(tabs, function(t, idx){
                t.addEventListener('click', function(){ activate(t.getAttribute('data-sf-mbtab')); });
                t.addEventListener('keydown', function(e){
                    var next = null;
                    if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = tabs[(idx+1)%tabs.length];
                    else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = tabs[(idx-1+tabs.length)%tabs.length];
                    else if (e.key === 'Home') next = tabs[0];
                    else if (e.key === 'End')  next = tabs[tabs.length-1];
                    if (next) { e.preventDefault(); next.focus(); next.click(); }
                });
            });

            // UX: when an inline error appears in a hidden panel (server-side
            // validation, or a Fix issue button click) — auto-reveal that panel.
            // We watch for new .notice-error or .form-invalid inside any panel.
            var mo = new MutationObserver(function(muts){
                muts.forEach(function(m){
                    Array.prototype.forEach.call(m.addedNodes || [], function(n){
                        if (n.nodeType !== 1) return;
                        if (n.matches && (n.matches('.notice-error') || n.matches('.form-invalid'))) {
                            var panel = n.closest('.sf-mb-panel');
                            if (panel) activate(panel.getAttribute('data-sf-mbpanel'));
                        }
                    });
                });
            });
            mo.observe(box, {childList:true, subtree:true});
        })();
        </script>
        <?php
    }
}

} // end: if (!class_exists('SEOFORGE_Metabox_View'))
