<?php
/**
 * Schema Manager
 *
 * Main controller that orchestrates all schema functionality
 *
 * @package TamRank
 * @subpackage Schema
 */

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

// Load dependencies
require_once plugin_dir_path(__FILE__) . 'schema-config.php';
require_once plugin_dir_path(__FILE__) . 'class-schema-settings.php';
require_once plugin_dir_path(__FILE__) . 'class-schema-templates.php';
require_once plugin_dir_path(__FILE__) . 'class-schema-meta-box.php';
require_once plugin_dir_path(__FILE__) . 'class-schema-render.php';
require_once plugin_dir_path(__FILE__) . 'class-schema-audit.php';

// Load Schema Detector module
require_once plugin_dir_path(__FILE__) . 'detector/class-detection-result.php';
require_once plugin_dir_path(__FILE__) . 'detector/class-detector.php';
require_once plugin_dir_path(__FILE__) . 'detector/class-storage.php';
require_once plugin_dir_path(__FILE__) . 'detector/class-manager.php';

class TAMRANK_Schema_Manager
{

    /**
     * Settings manager instance
     *
     * @var TAMRANK_Schema_Settings
     */
    private $settings;

    /**
     * Templates manager instance
     *
     * @var TAMRANK_Schema_Templates
     */
    private $templates;

    /**
     * Meta box instance
     *
     * @var TAMRANK_Schema_Meta_Box
     */
    private $meta_box;

    /**
     * Renderer instance
     *
     * @var TAMRANK_Schema_Render
     */
    private $renderer;

    /**
     * Audit instance
     *
     * @var TAMRANK_Schema_Audit
     */
    private $audit;

    /**
     * Detector manager instance
     *
     * @var TAMRANK_Schema_Detector_Manager
     */
    private $detector_manager;

    /**
     * Initialize the schema manager
     */
    public function __construct()
    {
        $this->settings = new TAMRANK_Schema_Settings();
        $this->templates = new TAMRANK_Schema_Templates();
        $this->meta_box = new TAMRANK_Schema_Meta_Box($this->templates);
        $this->renderer = new TAMRANK_Schema_Render($this->templates, $this->settings);
        $this->audit = new TAMRANK_Schema_Audit($this->templates, $this->settings);

        // Initialiseer Schema Detector Manager
        $this->detector_manager = TAMRANK_Schema_Detector_Manager::krijg_instantie();

        // Handle delete action EARLY via admin_init (before any output)
        add_action('admin_init', array($this, 'handle_delete_template'));

        // Handle form submissions (fallback for non-AJAX)
        add_action('admin_post_tamrank_save_schema_template', array($this, 'handle_save_template'));
        add_action('admin_post_tamrank_test_schema', array($this, 'handle_test_schema'));

        // Enqueue admin assets
        add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));

        // AJAX handlers
        add_action('wp_ajax_tamrank_validate_jsonld', array($this, 'ajax_validate_jsonld'));
        add_action('wp_ajax_tamrank_save_schema_template_ajax', array($this, 'ajax_save_template'));
        add_action('wp_ajax_tamrank_delete_schema_template_ajax', array($this, 'ajax_delete_template'));
        add_action('wp_ajax_tamrank_save_global_schema_ajax', array($this, 'ajax_save_global_settings'));
        add_action('wp_ajax_tamrank_preview_localbusiness_schema', array($this, 'ajax_preview_localbusiness'));

        // One-time notice after the localbusiness migration.
        add_action('admin_init', array($this, 'handle_output_location_notice'));
        add_action('admin_notices', array($this, 'render_output_location_notice'));

        // Background schema upgrade after PRO activation
        add_action('tamrank_run_schema_upgrade', array($this, 'process_schema_upgrade_batch'));
    }

    /**
     * Process a batch of posts for auto schema detection (background cron).
     * Called after PRO license activation to detect schema for all posts.
     */
    public function process_schema_upgrade_batch()
    {
        $pending = get_option('tam_rank_schema_upgrade_pending', array());
        if (empty($pending)) {
            return;
        }

        $batch = array_splice($pending, 0, 10);
        update_option('tam_rank_schema_upgrade_pending', $pending, false);

        foreach ($batch as $post_id) {
            $post = get_post((int) $post_id);
            if (!$post || $post->post_status !== 'publish') {
                continue;
            }
            try {
                $this->detector_manager->voer_detectie_uit($post);
            } catch (\Exception $e) {
                // Skip problematic posts silently
            }
        }

        // Schedule next batch if there are more
        if (!empty($pending)) {
            wp_schedule_single_event(time() + 10, 'tamrank_run_schema_upgrade');
        }
    }

    /**
     * Handle template deletion early via admin_init
     */
    public function handle_delete_template()
    {
        if (!isset($_GET['page']) || $_GET['page'] !== 'tam-rank-schema') {
            return;
        }
        if (!isset($_GET['action']) || $_GET['action'] !== 'delete') {
            return;
        }
        if (!isset($_GET['template_id']) || !isset($_GET['_wpnonce'])) {
            return;
        }
        if (!current_user_can('manage_options')) {
            return;
        }

        $template_id = sanitize_text_field(wp_unslash($_GET['template_id']));
        if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'delete_template_' . $template_id)) {
            return;
        }

        $this->templates->delete_template($template_id);
        wp_safe_redirect(admin_url('admin.php?page=tam-rank-schema&message=template_deleted'));
        exit;
    }

    /**
     * AJAX handler for JSON-LD validation
     */
    public function ajax_validate_jsonld()
    {
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'tamrank_schema_nonce')) {
            wp_send_json_error(array('message' => tamrank__('error_security_token', 'Invalid security token. Please refresh the page.'), 'code' => 'nonce_invalid'));
        }
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        $json_code = isset($_POST['json_code']) ? wp_unslash($_POST['json_code']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- raw JSON-LD consumed only by tamrank_validate_jsonld() (json_decode); never persisted or echoed as markup. Text sanitization would corrupt the JSON.
        $result = tamrank_validate_jsonld($json_code);
        unset($result['parsed']);
        wp_send_json_success($result);
    }

    /**
     * AJAX handler for template save
     */
    public function ajax_save_template()
    {
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'tamrank_schema_nonce')) {
            wp_send_json_error(array('message' => tamrank__('error_security_token', 'Invalid security token. Please refresh the page.'), 'code' => 'nonce_invalid'));
        }
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        $template_id = !empty($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : null;

        $template_data = array(
            'name' => sanitize_text_field(wp_unslash($_POST['name'] ?? '')),
            'schema_type' => sanitize_text_field(wp_unslash($_POST['schema_type'] ?? '')),
            'post_types' => isset($_POST['post_types']) ? array_map('sanitize_text_field', wp_unslash((array) $_POST['post_types'])) : array(),
            'field_mappings' => isset($_POST['field_mappings']) ? map_deep(wp_unslash($_POST['field_mappings']), 'sanitize_text_field') : array(),
            'custom_jsonld_enabled' => !empty($_POST['custom_jsonld_enabled']),
            'custom_jsonld_code' => isset($_POST['custom_jsonld_code']) ? wp_unslash($_POST['custom_jsonld_code']) : '' // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- raw JSON-LD validated below via tamrank_validate_jsonld() before storage, then stored verbatim and escaped on output (wp_json_encode JSON_HEX_*); text sanitization would corrupt the JSON.
        );

        // Validate-on-store: reject malformed custom JSON-LD before it is persisted.
        if (!empty($template_data['custom_jsonld_enabled']) && trim((string) $template_data['custom_jsonld_code']) !== '') {
            $jsonld_check = tamrank_validate_jsonld($template_data['custom_jsonld_code']);
            if (empty($jsonld_check['valid'])) {
                wp_send_json_error(array(
                    'message' => !empty($jsonld_check['errors']) ? implode(' ', $jsonld_check['errors']) : tamrank__('error_invalid_jsonld', 'The custom JSON-LD is not valid JSON.'),
                    'code'    => 'invalid_jsonld',
                ));
            }
        }

        if ($template_id) {
            $result = $this->templates->update_template($template_id, $template_data);
        } else {
            $result = $this->templates->create_template($template_data);
        }

        if (is_wp_error($result)) {
            wp_send_json_error(array('message' => $result->get_error_message(), 'code' => $result->get_error_code() ?: 'request_failed'));
        }

        // Return the saved template data for UI update
        $saved_id = $template_id ?: $result;
        $saved_template = $this->templates->get_template($saved_id);
        wp_send_json_success(array(
            'id' => $saved_id,
            'template' => $saved_template
        ));
    }

    /**
     * AJAX handler for template deletion
     */
    public function ajax_delete_template()
    {
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'tamrank_schema_nonce')) {
            wp_send_json_error(array('message' => tamrank__('error_security_token', 'Invalid security token. Please refresh the page.'), 'code' => 'nonce_invalid'));
        }
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        $template_id = sanitize_text_field(wp_unslash($_POST['template_id'] ?? ''));
        if (empty($template_id)) {
            wp_send_json_error(array('message' => tamrank__('error_no_template_id', 'No template selected.'), 'code' => 'missing_parameter'));
        }

        $deleted = $this->templates->delete_template($template_id);
        if ($deleted) {
            wp_send_json_success();
        } else {
            wp_send_json_error(array('message' => tamrank__('error_template_not_found', 'The template could not be found.'), 'code' => 'not_found'));
        }
    }

    /**
     * AJAX handler for global schema settings save
     */
    public function ajax_save_global_settings()
    {
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'tamrank_schema_nonce')) {
            wp_send_json_error(array('message' => tamrank__('error_security_token', 'Invalid security token. Please refresh the page.'), 'code' => 'nonce_invalid'));
        }
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        $new_settings = array(
            'entity_type' => sanitize_text_field(wp_unslash($_POST['entity_type'] ?? 'Organization')),
            'organization_name' => sanitize_text_field(wp_unslash($_POST['organization_name'] ?? '')),
            'website_url' => esc_url_raw(wp_unslash($_POST['website_url'] ?? '')),
            'email' => sanitize_email(wp_unslash($_POST['email'] ?? '')),
            'telephone' => sanitize_text_field(wp_unslash($_POST['telephone'] ?? '')),
            'logo_url' => esc_url_raw(wp_unslash($_POST['logo_url'] ?? '')),
            'default_social_image' => esc_url_raw(wp_unslash($_POST['default_social_image'] ?? '')),
            'default_social_image_id' => absint(wp_unslash($_POST['default_social_image_id'] ?? 0)),
            'custom_jsonld_code' => isset($_POST['custom_jsonld_code']) ? wp_unslash($_POST['custom_jsonld_code']) : '' // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- raw JSON-LD validated below via tamrank_validate_jsonld() before storage, then stored verbatim and escaped on output (wp_json_encode JSON_HEX_*); text sanitization would corrupt the JSON.
        );

        // Address
        if (isset($_POST['address']) && is_array($_POST['address'])) {
            $new_settings['address'] = array(
                'street' => sanitize_text_field(wp_unslash($_POST['address']['street'] ?? '')),
                'postal_code' => sanitize_text_field(wp_unslash($_POST['address']['postal_code'] ?? '')),
                'city' => sanitize_text_field(wp_unslash($_POST['address']['city'] ?? '')),
                'country' => sanitize_text_field(wp_unslash($_POST['address']['country'] ?? 'NL'))
            );
        }

        // Social profiles (sameAs) — the key existed and was read, but had no UI
        // and was never posted. Section 4 gives it one.
        if (isset($_POST['social_profiles']) && is_array($_POST['social_profiles'])) {
            $new_settings['social_profiles'] = TAMRANK_Schema_Settings::filter_profiles(
                array_map('esc_url_raw', wp_unslash($_POST['social_profiles'])) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- esc_url_raw() is the sanitizer for each member.
            );
        } else {
            $new_settings['social_profiles'] = array();
        }

        // LocalBusiness subtree. The form always posts it COMPLETE — see the
        // note in TAMRANK_Schema_Settings::update_settings() on the shallow
        // array_merge() there.
        if (isset($_POST['localbusiness']) && is_array($_POST['localbusiness'])) {
            $new_settings['localbusiness'] = TAMRANK_Schema_Settings::sanitize_localbusiness(
                map_deep(wp_unslash($_POST['localbusiness']), 'sanitize_text_field')
            );

            // There is one set of address inputs in the form, shared by
            // Organization and LocalBusiness mode. It writes to
            // locations[0].address; update_settings() mirrors that back into the
            // legacy 'address' key. One writer, no divergence.
            if (isset($new_settings['address'])) {
                $new_settings['localbusiness']['locations'][0]['address'] = array(
                    'street'      => $new_settings['address']['street'],
                    'postal_code' => $new_settings['address']['postal_code'],
                    'city'        => $new_settings['address']['city'],
                    'country'     => TAMRANK_Schema_Settings::normalize_country($new_settings['address']['country']),
                );
            }
        }

        // Merchant subtree (MERCH-01). The form posts it complete, same contract
        // as localbusiness; every value is whitelisted in sanitize_merchant().
        if (isset($_POST['merchant']) && is_array($_POST['merchant'])) {
            $new_settings['merchant'] = TAMRANK_Schema_Settings::sanitize_merchant(
                map_deep(wp_unslash($_POST['merchant']), 'sanitize_text_field')
            );
        }

        // Validate-on-store: reject malformed custom JSON-LD before it is persisted.
        if (trim((string) $new_settings['custom_jsonld_code']) !== '') {
            $jsonld_check = tamrank_validate_jsonld($new_settings['custom_jsonld_code']);
            if (empty($jsonld_check['valid'])) {
                wp_send_json_error(array(
                    'message' => !empty($jsonld_check['errors']) ? implode(' ', $jsonld_check['errors']) : tamrank__('error_invalid_jsonld', 'The custom JSON-LD is not valid JSON.'),
                    'code'    => 'invalid_jsonld',
                ));
            }
        }

        $this->settings->update_settings($new_settings);

        // Saving settles the migration question, so the notice has served its purpose.
        delete_option(TAMRANK_Schema_Settings::OUTPUT_NOTICE_OPTION);

        wp_send_json_success();
    }

    /**
     * AJAX handler for the live LocalBusiness preview.
     *
     * The panel shows what the PHP builder produces, not a second
     * implementation in JS — a JS copy would drift from the renderer the moment
     * either side changes. Nothing is persisted here.
     */
    public function ajax_preview_localbusiness()
    {
        if (!isset($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'tamrank_schema_nonce')) {
            wp_send_json_error(array('message' => tamrank__('error_security_token', 'Invalid security token. Please refresh the page.'), 'code' => 'nonce_invalid'));
        }
        if (!current_user_can('manage_options')) {
            wp_send_json_error(array('message' => tamrank__('error_permission_denied', 'You do not have permission for this action.'), 'code' => 'permission_denied'));
        }

        // Start from the stored settings so unposted keys keep their value,
        // then overlay exactly what the form sent.
        $settings = $this->settings->get_settings();

        $settings['organization_name'] = sanitize_text_field(wp_unslash($_POST['organization_name'] ?? ''));
        $settings['website_url']       = esc_url_raw(wp_unslash($_POST['website_url'] ?? ''));
        $settings['email']             = sanitize_email(wp_unslash($_POST['email'] ?? ''));
        $settings['telephone']         = sanitize_text_field(wp_unslash($_POST['telephone'] ?? ''));
        $settings['logo_url']          = esc_url_raw(wp_unslash($_POST['logo_url'] ?? ''));

        $settings['social_profiles'] = isset($_POST['social_profiles']) && is_array($_POST['social_profiles'])
            ? TAMRANK_Schema_Settings::filter_profiles(array_map('esc_url_raw', wp_unslash($_POST['social_profiles']))) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- esc_url_raw() is the sanitizer for each member.
            : array();

        if (isset($_POST['localbusiness']) && is_array($_POST['localbusiness'])) {
            $settings['localbusiness'] = TAMRANK_Schema_Settings::sanitize_localbusiness(
                map_deep(wp_unslash($_POST['localbusiness']), 'sanitize_text_field')
            );
        }
        if (isset($_POST['address']) && is_array($_POST['address'])) {
            $address = map_deep(wp_unslash($_POST['address']), 'sanitize_text_field');
            $settings['localbusiness']['locations'][0]['address'] = array(
                'street'      => $address['street'] ?? '',
                'postal_code' => $address['postal_code'] ?? '',
                'city'        => $address['city'] ?? '',
                'country'     => TAMRANK_Schema_Settings::normalize_country($address['country'] ?? ''),
            );
        }

        $node = $this->settings->get_localbusiness_schema($settings, true);

        wp_send_json_success(array('node' => $node));
    }

    /**
     * One-time notice after an existing install was migrated.
     *
     * The migration deliberately keeps such installs on 'all' — nobody's live
     * markup gets narrowed behind their back. This notice offers the switch
     * instead of taking it.
     */
    public function render_output_location_notice()
    {
        if (!current_user_can('manage_options')) {
            return;
        }
        if (get_option(TAMRANK_Schema_Settings::OUTPUT_NOTICE_OPTION) !== 'pending') {
            return;
        }
        $settings = $this->settings->get_settings();
        if (($settings['entity_type'] ?? '') !== 'LocalBusiness') {
            return;
        }

        // Plain links with a nonce, no JavaScript: an admin notice shows up on
        // every admin screen and should not depend on a script being enqueued there.
        $base = admin_url('admin.php?page=tam-rank-schema&tab=instellingen');
        $set  = wp_nonce_url(add_query_arg('tamrank_lb_output', 'homepage', $base), 'tamrank_lb_output_notice');
        $keep = wp_nonce_url(add_query_arg('tamrank_lb_output', 'keep', $base), 'tamrank_lb_output_notice');
        ?>
        <div class="notice notice-info tamrank-lb-output-notice">
            <p>
                <strong><?php tamrank_e('lb_notice_title', 'TamRank: your business markup now has its own settings'); ?></strong><br>
                <?php tamrank_e('lb_notice_text', 'Your LocalBusiness markup keeps rendering on every page, exactly as before — we did not change that for you. Google only looks for it on your homepage or contact page, so limiting it there is usually the tidier choice.'); ?>
            </p>
            <p>
                <a class="button button-primary" href="<?php echo esc_url($set); ?>"><?php tamrank_e('lb_notice_set_homepage', 'Set to homepage only'); ?></a>
                <a class="button" href="<?php echo esc_url($keep); ?>"><?php tamrank_e('lb_notice_keep', 'Keep it on all pages'); ?></a>
            </p>
        </div>
        <?php
    }

    /**
     * Handle the one-time notice's two choices. Either way the notice is gone
     * afterwards, so it is shown exactly once.
     */
    public function handle_output_location_notice()
    {
        if (!isset($_GET['tamrank_lb_output']) || !isset($_GET['_wpnonce'])) {
            return;
        }
        if (!current_user_can('manage_options')) {
            return;
        }
        if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'tamrank_lb_output_notice')) {
            return;
        }

        if (sanitize_text_field(wp_unslash($_GET['tamrank_lb_output'])) === 'homepage') {
            $settings = $this->settings->get_settings();
            $settings['localbusiness']['output_location'] = 'homepage';
            $this->settings->update_settings(array('localbusiness' => $settings['localbusiness']));
        }

        delete_option(TAMRANK_Schema_Settings::OUTPUT_NOTICE_OPTION);
        wp_safe_redirect(admin_url('admin.php?page=tam-rank-schema&tab=instellingen'));
        exit;
    }

    /**
     * Enqueue admin assets
     */
    public function enqueue_admin_assets($hook)
    {
        if (strpos($hook, 'tam-rank-schema') === false && $hook !== 'post.php' && $hook !== 'post-new.php') {
            return;
        }

        wp_enqueue_media();

        // Load detector scripts on post edit pages
        if (($hook === 'post.php' || $hook === 'post-new.php') && $this->detector_manager) {
            $this->detector_manager->registreer_admin_assets($hook);
        }
    }

    /* ================================================================
     *  ADMIN PAGE RENDERING
     * ================================================================ */

    /**
     * Render the main admin page
     */
    public function render_admin_page()
    {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html(tamrank__('error_permission_denied', 'You do not have permission for this action.')));
        }

        $nonce = wp_create_nonce('tamrank_schema_nonce');
        ?>
        <div class="tr-page-wrap tr-schema-wrap" data-nonce="<?php echo esc_attr($nonce); ?>">

            <div class="tr-schema-header">
                <h1 class="tr-schema-title"><?php tamrank_e('schema_builder_title', 'Schema Builder'); ?></h1>
                <p class="tr-schema-subtitle"><?php tamrank_e('schema_builder_subtitle', 'Structured data markup for better visibility in search engines'); ?></p>
            </div>

            <div class="tr-pill-nav" role="tablist">
                <button class="tr-pill is-active" data-tab="overzicht"><?php tamrank_e('tab_overview', 'Overview'); ?></button>
                <button class="tr-pill" data-tab="templates"><?php tamrank_e('tab_templates', 'Templates'); ?></button>
                <button class="tr-pill" data-tab="instellingen"><?php tamrank_e('tab_global_settings', 'Global Settings'); ?></button>
            </div>

            <div class="tr-tab-panel is-active" data-panel="overzicht">
                <?php $this->render_schema_overzicht(); ?>
            </div>

            <div class="tr-tab-panel" data-panel="templates">
                <?php $this->render_schema_templates(); ?>
            </div>

            <div class="tr-tab-panel" data-panel="instellingen">
                <?php $this->render_schema_instellingen(); ?>
            </div>

        </div>
        <?php
    }

    /**
     * Tab: Overzicht
     */
    private function render_schema_overzicht()
    {
        // Count pages with schema
        $recent_posts = get_posts(array(
            'numberposts' => 10,
            'post_status' => 'publish',
            'post_type' => 'any'
        ));

        $total_pages = count($recent_posts);
        $active_count = 0;
        $output_rows = array();

        foreach ($recent_posts as $post) {
            $enabled = get_post_meta($post->ID, 'tamrank_schema_enabled', true);
            $is_active = ($enabled === '1' || $enabled === '');
            if ($is_active) $active_count++;

            $template_id = get_post_meta($post->ID, 'tamrank_schema_template_id', true);
            $template = $template_id ? $this->templates->get_template($template_id) : null;
            $schema_bron = get_post_meta($post->ID, 'tamrank_schema_bron', true);

            $auto_type = get_post_meta($post->ID, '_tamrank_schema_auto_type', true);
            $auto_extras = get_post_meta($post->ID, '_tamrank_schema_auto_extras', true);

            if (empty($auto_type)) {
                $metabox_data = get_post_meta($post->ID, '_tamrank_schema_metabox_data', true);
                if (is_array($metabox_data) && isset($metabox_data['detectie']['hoofd_schema'])) {
                    $auto_type = $metabox_data['detectie']['hoofd_schema'];
                    $auto_extras = $metabox_data['detectie']['extra_schemas'] ?? array();
                }
            }
            if (empty($auto_type)) {
                $auto_type = get_post_meta($post->ID, 'tamrank_schema_type', true);
            }

            $display_schema_type = '';
            $display_extras = array();
            $is_from_auto = false;

            if ($template && $schema_bron === 'template') {
                $display_schema_type = $template['schema_type'];
            } elseif ($schema_bron === 'automatisch' && !empty($auto_type)) {
                $display_schema_type = $auto_type;
                $display_extras = is_array($auto_extras) ? $auto_extras : array();
                $is_from_auto = true;
            } elseif ($template) {
                $display_schema_type = $template['schema_type'];
            } elseif (!empty($auto_type)) {
                $display_schema_type = $auto_type;
                $display_extras = is_array($auto_extras) ? $auto_extras : array();
                $is_from_auto = true;
            }

            if (tamrank_post_type_has_breadcrumb_schema($post->post_type)) {
                if (!in_array('BreadcrumbList', $display_extras, true)) {
                    $display_extras[] = 'BreadcrumbList';
                }
            }

            // ÉÉN BRON VAN WAARHEID: filter de extra-chips op wat de renderer ECHT
            // bouwt — identiek aan de meta-box (admin/meta-boxes.php:1376-1388). De
            // detector beveelt extra's aan op score (bv. HowTo op titel), maar de
            // renderer dropt elk type zonder bruikbare content (HowTo zonder stappen),
            // zodat het niet in de JSON-LD belandt. Zo matchen de chips de output.
            if ($is_from_auto && !empty($display_extras)) {
                $chip_renderer = $this->get_renderer();
                if ($chip_renderer && method_exists($chip_renderer, 'get_renderable_types')) {
                    $renderbare = $chip_renderer->get_renderable_types($post, $schema_bron ?: 'automatisch');
                    // Empty-guard: bij een lege/mislukte build liever de detectie tonen
                    // dan alle chips verbergen (zelfde keuze als de meta-box).
                    if (!empty($renderbare)) {
                        $display_extras = array_values(array_intersect($display_extras, $renderbare));
                    }
                }
            }

            // Fallback: a post the detector never typed (e.g. a short, just-created
            // post that content detection skipped) still auto-renders its page type
            // (BlogPosting/Article) + BreadcrumbList. The stored detector type is
            // only a cache; the renderer is the single source of truth. So when we
            // have no type yet but the page is active, ask the renderer what it
            // actually outputs — otherwise the overview shows an active page with an
            // empty Schema Types column (which the live JSON-LD contradicts).
            if ($is_active && empty($display_schema_type)) {
                $type_renderer = $this->get_renderer();
                if ($type_renderer && method_exists($type_renderer, 'get_renderable_types')) {
                    $renderable = $type_renderer->get_renderable_types($post, $schema_bron ?: 'automatisch');
                    if (!empty($renderable)) {
                        $display_schema_type = array_shift($renderable);
                        $display_extras      = $renderable;
                        // Mirror the renderer's own branching: a 'template' bron
                        // resolved through the template path, so it is NOT auto.
                        // Everything else (automatisch/empty/handmatig) went through
                        // the auto-render path. Hardcoding true mislabeled a
                        // deleted-explicit-template row with the "auto" pill.
                        $is_from_auto        = ($schema_bron !== 'template');
                    }
                }
            }

            $pt_obj = get_post_type_object($post->post_type);
            $output_rows[] = array(
                'title' => get_the_title($post),
                'post_type_label' => $pt_obj ? $pt_obj->labels->singular_name : $post->post_type,
                'is_active' => $is_active,
                'schema_type' => $display_schema_type,
                'extras' => $display_extras,
                'is_auto' => $is_from_auto,
                'url' => get_permalink($post)
            );
        }

        $active_pct = $total_pages > 0 ? round(($active_count / $total_pages) * 100) : 0;
        ?>

        <!-- Stats bar -->
        <div class="tr-stats-grid">
            <div class="tr-stat-card">
                <div class="tr-stat-number"><?php echo esc_html($total_pages); ?></div>
                <div class="tr-stat-label"><?php tamrank_e('schema_pages_with_schema', 'Pages with schema'); ?></div>
                <div class="tr-stat-meta"><?php tamrank_e('schema_auto_generated', 'Automatically generated'); ?></div>
            </div>
            <div class="tr-stat-card">
                <div class="tr-stat-number is-primary"><?php echo esc_html($active_pct); ?>%</div>
                <div class="tr-stat-label"><?php tamrank_e('schema_active_label', 'Schema active'); ?></div>
                <div class="tr-stat-meta"><?php tamrank_e('schema_all_correct', 'Everything working correctly'); ?></div>
            </div>
            <div class="tr-test-url-card">
                <div class="tr-test-url-label"><?php tamrank_e('schema_test_url', 'Test a URL'); ?></div>
                <div class="tr-test-url-row">
                    <input type="url" class="tr-test-url-input" id="tr-test-url-input"
                           placeholder="<?php echo esc_attr__( 'https://example.com/page/', 'tam-rank' ); ?>">
                    <button class="tr-btn tr-btn--primary" id="tr-test-google-btn"><?php tamrank_e('schema_test_google', 'Test with Google'); ?></button>
                </div>
            </div>
        </div>

        <!-- Recent schema output -->
        <div class="tr-card">
            <div class="tr-card-header">
                <div>
                    <h2 class="tr-card-title"><?php tamrank_e('schema_recent_output', 'Recent Schema Output'); ?></h2>
                    <p class="tr-card-subtitle"><?php tamrank_e('schema_recent_output_desc', 'View the schema markup on your recent pages'); ?></p>
                </div>
            </div>
            <div class="tr-table-wrap">
                <table class="tr-table">
                    <thead>
                        <tr>
                            <th><?php tamrank_e('col_page', 'Page'); ?></th>
                            <th><?php tamrank_e('col_status', 'Status'); ?></th>
                            <th><?php tamrank_e('col_schema_types', 'Schema Types'); ?></th>
                            <th></th>
                        </tr>
                    </thead>
                    <tbody>
                    <?php if (!empty($output_rows)) : foreach ($output_rows as $item) : ?>
                        <tr>
                            <td>
                                <div class="tr-table-page-name"><?php echo esc_html($item['title']); ?></div>
                                <div class="tr-table-page-type"><?php echo esc_html($item['post_type_label']); ?></div>
                            </td>
                            <td>
                                <?php if ($item['is_active']) : ?>
                                    <span class="tr-status-active"><?php tamrank_e('active', 'Active'); ?></span>
                                <?php else : ?>
                                    <span class="tr-status-disabled"><?php tamrank_e('disabled', 'Disabled'); ?></span>
                                <?php endif; ?>
                            </td>
                            <td>
                                <?php if (!empty($item['schema_type'])) : ?>
                                <div class="tr-schema-pills">
                                    <span class="tr-schema-pill"><?php echo esc_html($item['schema_type']); ?></span>
                                    <?php foreach ($item['extras'] as $extra) : ?>
                                        <span class="tr-schema-pill"><?php echo esc_html($extra); ?></span>
                                    <?php endforeach; ?>
                                    <?php if ($item['is_auto']) : ?>
                                        <span class="tr-schema-pill-auto">auto</span>
                                    <?php endif; ?>
                                </div>
                                <?php endif; ?>
                            </td>
                            <td>
                                <a href="<?php echo esc_url($item['url']); ?>" target="_blank"
                                   class="tr-action-link" data-tr-tooltip="<?php echo esc_attr(tamrank__('view_page', 'View page')); ?>"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></a>
                            </td>
                        </tr>
                    <?php endforeach; else : ?>
                        <tr>
                            <td colspan="4" class="tr-table-empty">
                                <?php tamrank_e('schema_no_output', 'No schema output found yet.'); ?>
                            </td>
                        </tr>
                    <?php endif; ?>
                    </tbody>
                </table>
            </div>
        </div>
        <?php
    }

    /**
     * Tab: Templates
     */
    private function render_schema_templates()
    {
        $templates = $this->templates->get_templates();
        $post_types = get_post_types(array('public' => true), 'objects');
        $schema_types = tamrank_get_schema_types();
        $field_sources = tamrank_get_field_sources();

        // Featured schema types (shown as cards)
        $featured_types = array(
            'Article'       => array('icon' => '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>', 'desc' => tamrank__('schema_type_article_desc', 'Blog posts and news articles')),
            'Product'       => array('icon' => '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>', 'desc' => tamrank__('schema_type_product_desc', 'Webshop products')),
            'LocalBusiness' => array('icon' => '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><path d="M9 22V12h6v10"/><path d="M8 6h.01M16 6h.01M8 10h.01M16 10h.01"/></svg>', 'desc' => tamrank__('schema_type_localbusiness_desc', 'Physical locations and offices')),
            'FAQPage'       => array('icon' => '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', 'desc' => tamrank__('schema_type_faqpage_desc', 'Questions and answers')),
            'Event'         => array('icon' => '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>', 'desc' => tamrank__('schema_type_event_desc', 'Events with date and location')),
            'Recipe'        => array('icon' => '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 2v7c0 1.1.9 2 2 2h4a2 2 0 0 0 2-2V2"/><path d="M7 2v20"/><path d="M21 15V2a5 5 0 0 0-5 5v6c0 1.1.9 2 2 2h3zm0 0v7"/></svg>', 'desc' => tamrank__('schema_type_recipe_desc', 'Recipes with ingredients')),
        );

        // Remaining types (shown in dropdown)
        $remaining_types = array_diff_key($schema_types, $featured_types);
        ?>

        <!-- LIST VIEW -->
        <div id="tr-templates-list">
            <div class="tr-card">
                <div class="tr-card-header">
                    <h2 class="tr-card-title"><?php tamrank_e('tab_templates', 'Templates'); ?></h2>
                    <button class="tr-btn tr-btn--primary" id="tr-new-template-btn">+ <?php tamrank_e('new_template', 'New Template'); ?></button>
                </div>
                <div class="tr-table-wrap">
                    <table class="tr-table">
                        <thead>
                            <tr>
                                <th><?php tamrank_e('col_template_name', 'Template Name'); ?></th>
                                <th><?php tamrank_e('col_schema_type', 'Schema Type'); ?></th>
                                <th><?php tamrank_e('col_post_types', 'Post Types'); ?></th>
                                <th><?php tamrank_e('col_modified', 'Modified'); ?></th>
                                <th><?php tamrank_e('col_actions', 'Actions'); ?></th>
                            </tr>
                        </thead>
                        <tbody id="tr-templates-tbody">
                        <?php if (!empty($templates)) : foreach ($templates as $tpl_id => $tpl) : ?>
                            <tr id="tr-template-row-<?php echo esc_attr($tpl_id); ?>">
                                <td><?php echo esc_html($tpl['name']); ?></td>
                                <td><?php echo esc_html($schema_types[$tpl['schema_type']]['label'] ?? $tpl['schema_type']); ?></td>
                                <td>
                                    <div class="tr-schema-pills">
                                        <?php foreach ($tpl['post_types'] as $pt) :
                                            $pt_obj = get_post_type_object($pt);
                                        ?>
                                            <span class="tr-post-pill"><?php echo esc_html($pt_obj ? $pt_obj->labels->singular_name : $pt); ?></span>
                                        <?php endforeach; ?>
                                    </div>
                                </td>
                                <td><?php
                                    // Een template dat via add_template() of update_template()
                                    // is opgeslagen draagt altijd updated_at. Een rij uit een
                                    // oudere optiestructuur niet, en dan gaf strtotime(null)
                                    // false, waarna date_i18n op de HUIDIGE tijd terugviel: de
                                    // kolom toonde het tijdstip van het bezoek alsof dat de
                                    // wijzigingsdatum was. Stil fout is erger dan leeg.
                                    // Zelfde guard als admin/settings-page.php voor de
                                    // migratiedatum, zelfde streepje als de Links-kolom.
                                    $tpl_updated = (isset($tpl['updated_at']) && is_scalar($tpl['updated_at']) && '' !== $tpl['updated_at'])
                                        ? strtotime((string) $tpl['updated_at'])
                                        : false;
                                    if (false !== $tpl_updated) {
                                        echo esc_html(date_i18n('d-m-Y H:i', $tpl_updated));
                                    } else {
                                        printf(
                                            '<span aria-hidden="true">&mdash;</span><span class="screen-reader-text">%s</span>',
                                            esc_html(tamrank__('unknown', 'Unknown'))
                                        );
                                    }
                                ?></td>
                                <td>
                                    <button class="tr-action-icon-edit tr-edit-template-btn"
                                            data-id="<?php echo esc_attr($tpl_id); ?>"
                                            data-tr-tooltip="<?php echo esc_attr(tamrank__('edit', 'Edit')); ?>"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
                                    <button class="tr-action-icon-delete tr-delete-template-btn"
                                            data-id="<?php echo esc_attr($tpl_id); ?>"
                                            data-tr-tooltip="<?php echo esc_attr(tamrank__('delete', 'Delete')); ?>"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg></button>
                                </td>
                            </tr>
                        <?php endforeach; else : ?>
                            <tr id="tr-templates-empty-row">
                                <td colspan="5" class="tr-table-empty">
                                    <?php tamrank_e('no_templates', 'No templates created yet.'); ?>
                                </td>
                            </tr>
                        <?php endif; ?>
                        </tbody>
                    </table>
                </div>
            </div>
            <div class="tr-info-banner">
                <?php tamrank_e('schema_templates_info', 'Most pages automatically get the correct schema. Templates are for specific customizations or advanced configurations.'); ?>
            </div>
        </div>

        <!-- CREATE/EDIT FORM -->
        <div id="tr-template-form" style="display:none;">
            <a href="#" class="tr-back-link" id="tr-back-to-list"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> <?php tamrank_e('back_to_templates', 'Back to Templates'); ?></a>

            <div class="tr-card">
                <!-- Step indicator -->
                <div class="tr-steps">
                    <div class="tr-step">
                        <div class="tr-step-circle is-active" id="tr-step-circle-1">1</div>
                        <span class="tr-step-label" id="tr-step-label-1"><?php tamrank_e('step_basics', 'Set basics'); ?></span>
                    </div>
                    <div class="tr-step-connector" id="tr-step-connector"></div>
                    <div class="tr-step">
                        <div class="tr-step-circle is-inactive" id="tr-step-circle-2">2</div>
                        <span class="tr-step-label is-inactive" id="tr-step-label-2"><?php tamrank_e('step_field_mappings', 'Field Mappings'); ?></span>
                    </div>
                </div>

                <!-- STEP 1 -->
                <div id="tr-wizard-step-1">
                    <div class="tr-form-grid">
                        <div class="tr-form-field">
                            <label class="tr-form-label">
                                <?php tamrank_e('template_name_label', 'Template Name'); ?> <span class="tr-required">*</span>
                            </label>
                            <input type="text" class="tr-form-input" id="tr-tpl-name"
                                   placeholder="<?php echo esc_attr(tamrank__('template_name_placeholder', 'E.g. Blog post template')); ?>">
                        </div>
                    </div>

                    <div class="tr-form-field" style="margin-top:20px;">
                        <label class="tr-form-label"><?php tamrank_e('schema_type_label', 'Schema Type'); ?> <span class="tr-required">*</span></label>
                        <p class="tr-card-subtitle" style="margin:4px 0 12px;"><?php tamrank_e('schema_type_choose_desc', 'Choose the type of structured data for this template'); ?></p>

                        <div class="tr-schema-type-grid">
                            <?php foreach ($featured_types as $type_key => $type_data) : ?>
                            <button class="tr-schema-type-card" data-type="<?php echo esc_attr($type_key); ?>">
                                <span class="tr-schema-type-icon"><?php echo wp_kses( $type_data['icon'], tam_rank_kses_svg() ); ?></span>
                                <div class="tr-schema-type-name"><?php echo esc_html($schema_types[$type_key]['label']); ?></div>
                                <div class="tr-schema-type-desc"><?php echo esc_html($type_data['desc']); ?></div>
                                <div class="tr-schema-type-selected-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> <?php tamrank_e('selected', 'Selected'); ?></div>
                            </button>
                            <?php endforeach; ?>
                        </div>

                        <?php if (!empty($remaining_types)) : ?>
                        <div class="tr-more-types-row">
                            <select class="tr-more-types-select" id="tr-more-types-select">
                                <option value=""><?php tamrank_e('more_schema_types', 'More schema types...'); ?></option>
                                <?php foreach ($remaining_types as $type_key => $type_cfg) : ?>
                                    <option value="<?php echo esc_attr($type_key); ?>">
                                        <?php echo esc_html($type_cfg['label']); ?> — <?php echo esc_html($type_cfg['description']); ?>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                        </div>
                        <?php endif; ?>
                    </div>

                    <div class="tr-form-field" style="margin-top:20px;">
                        <label class="tr-form-label"><?php tamrank_e('post_types_label', 'Post Types'); ?> <span class="tr-required">*</span></label>
                        <p class="tr-card-subtitle" style="margin:4px 0 12px;"><?php tamrank_e('post_types_choose_desc', 'Which content does this schema apply to?'); ?></p>
                        <div class="tr-checkbox-group" id="tr-post-types-checkboxes">
                            <?php foreach ($post_types as $pt_slug => $pt_obj) : ?>
                            <label class="tr-checkbox-label">
                                <input type="checkbox" name="tr_post_types[]"
                                       value="<?php echo esc_attr($pt_slug); ?>">
                                <?php echo esc_html($pt_obj->label); ?>
                            </label>
                            <?php endforeach; ?>
                        </div>
                    </div>

                    <input type="hidden" id="tr-edit-template-id" value="">

                    <div class="tr-form-footer">
                        <button class="tr-btn tr-btn--ghost" id="tr-cancel-step1"><?php tamrank_e('cancel', 'Cancel'); ?></button>
                        <button class="tr-btn tr-btn--primary" id="tr-to-step2-btn"><?php tamrank_e('next_field_mappings', 'Next: Field Mappings'); ?> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></button>
                    </div>
                </div>

                <!-- STEP 2 -->
                <div id="tr-wizard-step-2" style="display:none;">
                    <div class="tr-field-section-label"><?php tamrank_e('required_fields', 'Required fields'); ?></div>
                    <div id="tr-required-fields"></div>

                    <div class="tr-field-section-label" style="margin-top:16px;"><?php tamrank_e('optional_fields', 'Optional fields'); ?></div>
                    <div id="tr-optional-fields"></div>

                    <div class="tr-form-footer tr-form-footer-split">
                        <button class="tr-btn tr-btn--ghost" id="tr-to-step1-btn"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg> <?php tamrank_e('previous_step', 'Previous step'); ?></button>
                        <button class="tr-btn tr-btn--primary" id="tr-save-template-btn"><?php tamrank_e('template_save_btn', 'Save Template'); ?> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></button>
                    </div>
                </div>
            </div>
        </div>

        <?php
        // Schema-builder data is passed via the enqueued tamrank-schema-builder handle
        // instead of inline <script type="application/json"> data islands (wp.org compliance).
        $field_defs = array();
        foreach ($schema_types as $type_key => $type_cfg) {
            if (isset($type_cfg['is_custom']) && $type_cfg['is_custom']) continue;
            $required = array();
            $optional = array();
            foreach ($type_cfg['properties'] as $prop_key => $prop) {
                $entry = array(
                    'key' => $prop_key,
                    'label' => $prop['label'],
                    'type' => $prop['type'],
                    'required' => !empty($prop['required']),
                    'sources' => $prop['sources']
                );
                if (!empty($prop['required'])) {
                    $required[] = $entry;
                } else {
                    $optional[] = $entry;
                }
            }
            $field_defs[$type_key] = array('required' => $required, 'optional' => $optional);
        }
        wp_add_inline_script(
            'tamrank-schema-builder',
            'window.tamrankSchemaData = ' . wp_json_encode(array(
                'fieldDefs'         => $field_defs,
                'fieldSources'      => $field_sources,
                'existingTemplates' => $templates,
            ), JSON_HEX_TAG | JSON_HEX_AMP) . ';',
            'before'
        );
    }

    /**
     * Tab: Globale Instellingen
     */
    private function render_schema_instellingen()
    {
        $saved = $this->settings->get_settings();
        $entity_type = $saved['entity_type'] ?? 'Organization';

        $lb       = $saved['localbusiness'];
        $location = $lb['locations'][0];
        $is_lb    = ($entity_type === 'LocalBusiness');
        ?>
        <div class="tr-lb-bench<?php echo $is_lb ? ' is-lb' : ''; ?>" id="tr-global-bench">
        <div class="tr-lb-formcol">
        <div class="tr-card">
            <h2 class="tr-card-title"><?php tamrank_e('website_identity', 'Website Identity'); ?></h2>
            <p class="tr-card-subtitle" style="margin-bottom:20px;">
                <?php tamrank_e('website_identity_desc', 'Choose how your website presents itself in search engines'); ?>
            </p>

            <!-- Entity type cards -->
            <div class="tr-entity-grid">
                <button class="tr-entity-card <?php echo esc_attr( $entity_type === 'Organization' ? 'is-selected' : '' ); ?>"
                        data-entity="Organization">
                    <span class="tr-entity-icon"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/><line x1="12" y1="12" x2="12" y2="12.01"/></svg></span>
                    <div class="tr-entity-name">Organization</div>
                    <div class="tr-entity-desc"><?php tamrank_e('entity_org_desc', 'Online company, agency or brand. No fixed location needed.'); ?></div>
                    <div class="tr-entity-selected-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> <?php tamrank_e('selected', 'Selected'); ?></div>
                </button>

                <button class="tr-entity-card <?php echo esc_attr( $entity_type === 'LocalBusiness' ? 'is-selected' : '' ); ?>"
                        data-entity="LocalBusiness">
                    <span class="tr-entity-icon"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg></span>
                    <div class="tr-entity-name">LocalBusiness</div>
                    <div class="tr-entity-desc"><?php tamrank_e('entity_local_desc', 'Physical location, shop or office with address and opening hours.'); ?></div>
                    <div class="tr-entity-selected-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> <?php tamrank_e('selected', 'Selected'); ?></div>
                </button>

                <button class="tr-entity-card is-advanced <?php echo esc_attr( $entity_type === 'Custom' ? 'is-selected' : '' ); ?>"
                        data-entity="Custom">
                    <span class="tr-advanced-badge"><?php tamrank_e('advanced', 'Advanced'); ?></span>
                    <span class="tr-entity-icon"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg></span>
                    <div class="tr-entity-name">Custom JSON-LD</div>
                    <div class="tr-entity-desc"><?php tamrank_e('entity_custom_desc', 'Write your own complete JSON-LD schema markup. For advanced users only.'); ?></div>
                    <div class="tr-entity-selected-badge"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> <?php tamrank_e('selected', 'Selected'); ?></div>
                </button>
            </div>

            <!-- Standard form fields -->
            <div id="tr-entity-form" style="<?php echo esc_attr( $entity_type === 'Custom' ? 'display:none;' : '' ); ?>">

                <!-- ── Section 1 · always open ── -->
                <div class="tr-lb-section" data-lb-section="1">
                    <div class="tr-lb-shead tr-lb-only">
                        <div>
                            <span class="tr-lb-kicker"><?php tamrank_e('lb_section_1_kicker', 'Step 1'); ?></span>
                            <h3 class="tr-lb-stitle"><?php tamrank_e('lb_section_1_title', 'Business basics'); ?></h3>
                        </div>
                    </div>
                    <p class="tr-lb-lead tr-lb-only"><?php tamrank_e('lb_section_1_lead', 'Start with what kind of business you are — that shapes the rest of this form. Name, URL, email, phone and logo are the same details Organization uses; change them here and they change everywhere.'); ?></p>

                    <div class="tr-form-grid tr-lb-only" style="margin-bottom:16px;">
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('lb_subtype_label', 'Business type'); ?> <span class="tr-required">*</span></label>
                            <select class="tr-form-input" name="tr_lb_subtype" id="tr-lb-subtype">
                                <?php foreach (TAMRANK_Schema_Settings::get_subtype_whitelist() as $subtype) : ?>
                                    <option value="<?php echo esc_attr($subtype); ?>" <?php selected($location['subtype'], $subtype); ?>>
                                        <?php echo esc_html($subtype === 'LocalBusiness'
                                            ? $subtype . ' — ' . tamrank__('lb_subtype_generic', 'general')
                                            : $subtype); ?>
                                    </option>
                                <?php endforeach; ?>
                            </select>
                            <span class="tr-lb-help"><?php tamrank_e('lb_subtype_help', 'One type, as specific as you can — you will see it change in "@type" on the right. Not a list.'); ?></span>
                        </div>

                        <div class="tr-form-grid" id="tr-lb-food-fields" style="<?php echo esc_attr(in_array($location['subtype'], array('Restaurant', 'FoodEstablishment'), true) ? '' : 'display:none;'); ?>">
                            <div class="tr-lb-note tr-lb-note--ok">
                                <span aria-hidden="true">✦</span>
                                <span><b><?php tamrank_e('lb_food_note_title', 'Food and drink only'); ?></b>
                                <?php tamrank_e('lb_food_note_text', 'Google uses these two fields for Restaurant and FoodEstablishment only.'); ?></span>
                            </div>
                            <div class="tr-form-grid-2col">
                                <div class="tr-form-field">
                                    <label class="tr-form-label"><?php tamrank_e('lb_cuisine_label', 'Serves cuisine'); ?></label>
                                    <input type="text" class="tr-form-input" name="tr_lb_cuisine" id="tr-lb-cuisine"
                                           value="<?php echo esc_attr(implode(', ', $location['serves_cuisine'])); ?>"
                                           placeholder="<?php echo esc_attr(tamrank__('lb_cuisine_placeholder', 'Italian, pizza')); ?>">
                                </div>
                                <div class="tr-form-field">
                                    <label class="tr-form-label"><?php tamrank_e('lb_reservations_label', 'Accepts reservations'); ?></label>
                                    <select class="tr-form-input" name="tr_lb_reservations" id="tr-lb-reservations">
                                        <option value="" <?php selected($location['accepts_reservations'], ''); ?>><?php tamrank_e('lb_not_specified', '— not specified —'); ?></option>
                                        <option value="True" <?php selected($location['accepts_reservations'], 'True'); ?>><?php tamrank_e('yes', 'Yes'); ?></option>
                                        <option value="False" <?php selected($location['accepts_reservations'], 'False'); ?>><?php tamrank_e('no', 'No'); ?></option>
                                    </select>
                                </div>
                            </div>
                        </div>
                    </div>

                    <div class="tr-field-section-label tr-lb-hide"><?php tamrank_e('org_details', 'Organization details'); ?></div>
                    <div class="tr-form-grid">
                        <div class="tr-form-grid-2col">
                            <div class="tr-form-field">
                                <label class="tr-form-label"><?php tamrank_e('org_name_label', 'Organization name'); ?> <span class="tr-required">*</span></label>
                                <input type="text" class="tr-form-input" name="tr_org_name"
                                       value="<?php echo esc_attr($saved['organization_name']); ?>" placeholder="<?php echo esc_attr(tamrank__('org_name_placeholder', 'E.g. TamRank')); ?>">
                            </div>
                            <div class="tr-form-field">
                                <label class="tr-form-label"><?php tamrank_e('website_url_label', 'Website URL'); ?> <span class="tr-required">*</span></label>
                                <input type="url" class="tr-form-input" name="tr_org_url"
                                       value="<?php echo esc_attr($saved['website_url']); ?>" placeholder="<?php echo esc_attr(tamrank__('website_url_placeholder', 'https://www.yoursite.com')); ?>">
                            </div>
                        </div>
                        <div class="tr-form-grid-2col">
                            <div class="tr-form-field">
                                <label class="tr-form-label"><?php tamrank_e('email_label', 'Email address'); ?></label>
                                <input type="email" class="tr-form-input" name="tr_org_email"
                                       value="<?php echo esc_attr($saved['email']); ?>" placeholder="<?php echo esc_attr(tamrank__('email_placeholder', 'info@yoursite.com')); ?>">
                            </div>
                            <div class="tr-form-field">
                                <label class="tr-form-label"><?php tamrank_e('phone_label', 'Phone number'); ?></label>
                                <input type="tel" class="tr-form-input" name="tr_org_telephone"
                                       value="<?php echo esc_attr($saved['telephone']); ?>" placeholder="+31 6 12345678">
                            </div>
                        </div>
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('logo_label', 'Logo'); ?></label>
                            <input type="text" class="tr-form-input" name="tr_org_logo" id="tr-logo-url"
                                   value="<?php echo esc_attr($saved['logo_url']); ?>"
                                   placeholder="<?php echo esc_attr(tamrank__('logo_no_image', 'No image selected yet')); ?>">
                            <button type="button" class="tr-btn tr-btn--ghost" id="tr-upload-logo-btn"
                                    style="margin-top:6px; align-self:flex-start;"><?php tamrank_e('upload', 'Upload'); ?></button>
                        </div>
                        <div class="tr-form-field">
                            <label class="tr-form-label" for="tr-social-fallback-url"><?php tamrank_e('social_fallback_label', 'Fallback image for sharing'); ?></label>
                            <input type="text" class="tr-form-input" name="tr_social_fallback" id="tr-social-fallback-url"
                                   value="<?php echo esc_attr($saved['default_social_image'] ?? ''); ?>"
                                   placeholder="<?php echo esc_attr(tamrank__('logo_no_image', 'No image selected yet')); ?>">
                            <input type="hidden" id="tr-social-fallback-id" value="<?php echo esc_attr($saved['default_social_image_id'] ?? 0); ?>">
                            <button type="button" class="tr-btn tr-btn--ghost" id="tr-upload-social-fallback-btn"
                                    style="margin-top:6px; align-self:flex-start;"><?php tamrank_e('upload', 'Upload'); ?></button>
                            <p class="tr-form-help"><?php tamrank_e('social_fallback_help', 'When someone shares a page that has no image of its own, Facebook, X and LinkedIn show this one. Leave it empty and those pages get no image at all. Recommended size 1200 by 630 pixels.'); ?></p>
                        </div>
                    </div>
                </div>

                <!-- ── Section 2 · always open ── -->
                <div class="tr-lb-section" data-lb-section="2">
                    <div class="tr-lb-shead tr-lb-only">
                        <div>
                            <span class="tr-lb-kicker"><?php tamrank_e('lb_section_2_kicker', 'Step 2'); ?></span>
                            <h3 class="tr-lb-stitle"><?php tamrank_e('lb_section_2_title', 'Address and service area'); ?></h3>
                        </div>
                    </div>
                    <p class="tr-lb-lead tr-lb-only"><?php tamrank_e('lb_section_2_lead', 'Google needs one of two things: an address customers come to, or an area you work in. Set the switch below to whatever is true for you.'); ?></p>

                    <div class="tr-lb-switch tr-lb-only">
                        <button type="button" class="tr-lb-track<?php echo !empty($location['customers_visit']) ? ' is-on' : ''; ?>"
                                id="tr-lb-visit-toggle" role="switch"
                                aria-checked="<?php echo !empty($location['customers_visit']) ? 'true' : 'false'; ?>"
                                aria-label="<?php echo esc_attr(tamrank__('lb_visit_title', 'Customers visit this location')); ?>"></button>
                        <div class="tr-lb-switch-txt">
                            <strong><?php tamrank_e('lb_visit_title', 'Customers visit this location'); ?></strong>
                            <span id="tr-lb-visit-sub"><?php echo !empty($location['customers_visit'])
                                ? esc_html(tamrank__('lb_visit_sub_on', 'Your address ships as a PostalAddress — check the output on the right.'))
                                : esc_html(tamrank__('lb_visit_sub_off', 'Your address stays saved, but does not ship. Only your service area does.')); ?></span>
                        </div>
                    </div>

                    <div class="tr-lb-note tr-lb-note--warn tr-lb-only" id="tr-lb-visit-off-note"
                         style="<?php echo esc_attr(!empty($location['customers_visit']) ? 'display:none;' : ''); ?>">
                        <span aria-hidden="true">⚠</span>
                        <span><b><?php tamrank_e('lb_visit_off_title', 'No address means no local rich result'); ?></b>
                        <?php tamrank_e('lb_visit_off_text', 'Your address disappears from the output — exactly what you want if you work at the customer\'s location. The markup stays valid and Google still reads it, but the local rich result with map, opening hours and a directions button will not appear: Google needs a street address for that. Your service area therefore becomes required; that is what Google uses instead.'); ?></span>
                    </div>

                    <div class="tr-field-section-label tr-lb-hide" style="margin-top:20px;"><?php tamrank_e('address_details', 'Address details'); ?></div>
                    <div class="tr-form-grid<?php echo empty($location['customers_visit']) ? ' tr-lb-dim' : ''; ?>" id="tr-lb-address-fields">
                        <div class="tr-form-grid-2col">
                            <div class="tr-form-field">
                                <label class="tr-form-label"><?php tamrank_e('street_label', 'Street'); ?> <span class="tr-required tr-lb-addr-req tr-lb-only tr-lb-only-inline">*</span></label>
                                <input type="text" class="tr-form-input" name="tr_address_street"
                                       value="<?php echo esc_attr($saved['address']['street'] ?? ''); ?>">
                            </div>
                            <div class="tr-form-field">
                                <label class="tr-form-label"><?php tamrank_e('postal_code_label', 'Postal code'); ?> <span class="tr-required tr-lb-addr-req tr-lb-only tr-lb-only-inline">*</span></label>
                                <input type="text" class="tr-form-input" name="tr_address_postal"
                                       value="<?php echo esc_attr($saved['address']['postal_code'] ?? ''); ?>">
                            </div>
                        </div>
                        <div class="tr-form-grid-2col">
                            <div class="tr-form-field">
                                <label class="tr-form-label"><?php tamrank_e('city_label', 'City'); ?> <span class="tr-required tr-lb-addr-req tr-lb-only tr-lb-only-inline">*</span></label>
                                <input type="text" class="tr-form-input" name="tr_address_city"
                                       value="<?php echo esc_attr($saved['address']['city'] ?? ''); ?>">
                            </div>
                            <div class="tr-form-field">
                                <label class="tr-form-label"><?php tamrank_e('country_label', 'Country'); ?> <span class="tr-required tr-lb-addr-req tr-lb-only tr-lb-only-inline">*</span></label>
                                <?php $this->render_country_select($saved['address']['country'] ?? 'NL'); ?>
                            </div>
                        </div>
                    </div>

                    <div class="tr-form-field tr-lb-only" style="margin-top:16px;">
                        <label class="tr-form-label"><?php tamrank_e('lb_area_label', 'Service area'); ?>
                            <span class="tr-required tr-lb-area-req"<?php echo !empty($location['customers_visit']) ? ' style="display:none;"' : ''; ?>>*</span></label>
                        <input type="text" class="tr-form-input" name="tr_lb_area" id="tr-lb-area"
                               value="<?php echo esc_attr(implode(', ', $location['area_served'])); ?>"
                               placeholder="<?php echo esc_attr(tamrank__('lb_area_placeholder', 'Naarden, Bussum, Huizen')); ?>">
                        <span class="tr-lb-help"><?php tamrank_e('lb_area_help', 'Comma-separated towns or regions, shipped as areaServed.'); ?></span>
                    </div>
                </div>

                <?php $this->render_lb_sections_3_to_5($location); ?>
            </div>

            <!-- Custom JSON-LD section -->
            <div id="tr-custom-jsonld-section" <?php echo $entity_type !== 'Custom' ? 'style="display:none;"' : ''; ?>>
                <div class="tr-warning-banner is-visible">
                    <span class="tr-warning-icon"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
                    <div>
                        <div class="tr-warning-title"><?php tamrank_e('warning_advanced_feature', 'Warning: Advanced feature'); ?></div>
                        <div class="tr-warning-text">
                            <?php tamrank_e('custom_jsonld_warning', 'Custom JSON-LD replaces the default schema output. Incorrect JSON may be ignored by Google or harm your SEO. Use this only if you know what you are doing.'); ?>
                        </div>
                    </div>
                </div>
                <div class="tr-field-section-label">JSON-LD <?php tamrank_e('code_label', 'Code'); ?></div>
                <textarea class="tr-code-editor" name="tr_custom_jsonld" id="tr-custom-jsonld-editor"
                          spellcheck="false"><?php echo esc_textarea($saved['custom_jsonld_code'] ?? ''); ?></textarea>
                <div style="margin-top:10px;">
                    <button type="button" class="tr-btn tr-btn--secondary" id="tr-validate-jsonld-btn">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> <?php tamrank_e('validate_jsonld', 'Validate JSON-LD'); ?>
                    </button>
                </div>
            </div>

            <input type="hidden" name="tr_entity_type" id="tr-entity-type-input"
                   value="<?php echo esc_attr($entity_type); ?>">

            <div class="tr-form-footer">
                <button type="button" class="tr-btn tr-btn--primary" id="tr-save-global-btn">
                    <?php tamrank_e('settings_save_btn', 'Save Settings'); ?> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
                </button>
                <span class="tr-lb-unsaved" id="tr-lb-unsaved" style="display:none;"><?php tamrank_e('lb_unsaved_changes', 'Unsaved changes'); ?></span>
            </div>
        </div>

        <?php $this->render_merchant_card($saved['merchant']); ?>
        </div><!-- /.tr-lb-formcol -->

        <?php $this->render_lb_panel($lb); ?>
        </div><!-- /.tr-lb-bench -->
        <?php
    }

    /**
     * Merchant listing card: shipping, returns and product condition (MERCH-01).
     *
     * Only rendered when WooCommerce is active. Without a shop there is no
     * product schema to enrich, and a settings card that can never do anything
     * is worse than no card.
     *
     * @param array $merchant Merchant settings subtree.
     */
    private function render_merchant_card($merchant)
    {
        if (!function_exists('wc_get_product')) {
            return;
        }

        $conditions = array(
            'https://schema.org/NewCondition'         => tamrank__('merch_condition_new', 'New'),
            'https://schema.org/RefurbishedCondition' => tamrank__('merch_condition_refurbished', 'Refurbished'),
            'https://schema.org/UsedCondition'        => tamrank__('merch_condition_used', 'Used'),
        );
        $categories = array(
            'https://schema.org/MerchantReturnFiniteReturnWindow' => tamrank__('merch_return_finite', 'Returns within a set number of days'),
            'https://schema.org/MerchantReturnUnlimitedWindow'    => tamrank__('merch_return_unlimited', 'Returns without a deadline'),
            'https://schema.org/MerchantReturnNotPermitted'       => tamrank__('merch_return_none', 'No returns accepted'),
        );
        $methods = array(
            'https://schema.org/ReturnByMail'  => tamrank__('merch_method_mail', 'By post'),
            'https://schema.org/ReturnInStore' => tamrank__('merch_method_store', 'In the shop'),
            'https://schema.org/ReturnAtKiosk' => tamrank__('merch_method_kiosk', 'At a pickup point'),
        );
        $fees = array(
            'https://schema.org/FreeReturn'                     => tamrank__('merch_fees_free', 'Free of charge'),
            'https://schema.org/ReturnFeesCustomerResponsibility' => tamrank__('merch_fees_customer', 'Customer pays the return shipping'),
            'https://schema.org/ReturnShippingFees'             => tamrank__('merch_fees_fixed', 'Customer pays a fixed return fee'),
        );

        $shipping_on = !empty($merchant['shipping_enabled']);
        $returns_on = !empty($merchant['return_enabled']);
        $finite     = ($merchant['return_category'] === 'https://schema.org/MerchantReturnFiniteReturnWindow');
        $fixed_fee  = ($merchant['return_fees'] === 'https://schema.org/ReturnShippingFees');
        ?>
        <div class="tr-card tr-merch-card" id="tr-merch-card">
            <h2 class="tr-card-title"><?php tamrank_e('merch_card_title', 'Webshop listing'); ?></h2>
            <p class="tr-card-subtitle" style="margin-bottom:20px;">
                <?php tamrank_e('merch_card_subtitle', 'Shipping, returns and product condition in the product markup. These are the three things Google asks a webshop for on top of price and stock.'); ?>
            </p>

            <!-- ── Shipping ── -->
            <div class="tr-merch-section">
                <div class="tr-lb-switch">
                    <button type="button" class="tr-lb-track<?php echo !empty($merchant['shipping_enabled']) ? ' is-on' : ''; ?>"
                            id="tr-merch-shipping-toggle" role="switch"
                            aria-checked="<?php echo !empty($merchant['shipping_enabled']) ? 'true' : 'false'; ?>"
                            aria-label="<?php echo esc_attr(tamrank__('merch_shipping_title', 'Send shipping costs from WooCommerce')); ?>"></button>
                    <div class="tr-lb-switch-txt">
                        <strong><?php tamrank_e('merch_shipping_title', 'Send shipping costs from WooCommerce'); ?></strong>
                        <span><?php tamrank_e('merch_shipping_sub', 'Read straight from your shipping zones. Nothing is invented: a zone without a fixed amount stays out of the markup.'); ?></span>
                    </div>
                </div>

                <div class="tr-lb-note" id="tr-merch-shipping-note" style="<?php echo $shipping_on ? '' : 'display:none;'; ?>">
                    <span aria-hidden="true">ℹ</span>
                    <span><?php tamrank_e('merch_shipping_rules', 'A zone ships along when it names a country and charges a fixed amount. Skipped: a flat rate written as a calculation, free shipping that needs a minimum order or a coupon, pickup, and zones set by continent or postcode, because those carry no country code. With prices shown including tax the tax is added at your shop address.'); ?></span>
                </div>

                <div class="tr-form-grid" id="tr-merch-times" style="<?php echo $shipping_on ? '' : 'display:none;'; ?>">
                    <div class="tr-form-grid-2col">
                        <div class="tr-form-field">
                            <label class="tr-form-label" for="tr-merch-handling-min"><?php tamrank_e('merch_handling_label', 'Processing time in working days'); ?></label>
                            <div class="tr-merch-range">
                                <input type="number" min="0" max="365" class="tr-form-input" id="tr-merch-handling-min"
                                       value="<?php echo esc_attr($merchant['handling_time_min']); ?>"
                                       placeholder="<?php echo esc_attr(tamrank__('merch_min', 'min')); ?>"
                                       aria-label="<?php echo esc_attr(tamrank__('merch_handling_min_aria', 'Processing time, shortest')); ?>">
                                <span class="tr-merch-dash"><?php tamrank_e('merch_to', 'to'); ?></span>
                                <input type="number" min="0" max="365" class="tr-form-input" id="tr-merch-handling-max"
                                       value="<?php echo esc_attr($merchant['handling_time_max']); ?>"
                                       placeholder="<?php echo esc_attr(tamrank__('merch_max', 'max')); ?>"
                                       aria-label="<?php echo esc_attr(tamrank__('merch_handling_max_aria', 'Processing time, longest')); ?>">
                            </div>
                            <span class="tr-lb-help"><?php tamrank_e('merch_handling_help', 'From order to handover to the carrier. Leave empty and the delivery time stays out of the markup.'); ?></span>
                        </div>
                        <div class="tr-form-field">
                            <label class="tr-form-label" for="tr-merch-transit-min"><?php tamrank_e('merch_transit_label', 'Delivery time in working days'); ?></label>
                            <div class="tr-merch-range">
                                <input type="number" min="0" max="365" class="tr-form-input" id="tr-merch-transit-min"
                                       value="<?php echo esc_attr($merchant['transit_time_min']); ?>"
                                       placeholder="<?php echo esc_attr(tamrank__('merch_min', 'min')); ?>"
                                       aria-label="<?php echo esc_attr(tamrank__('merch_transit_min_aria', 'Delivery time, shortest')); ?>">
                                <span class="tr-merch-dash"><?php tamrank_e('merch_to', 'to'); ?></span>
                                <input type="number" min="0" max="365" class="tr-form-input" id="tr-merch-transit-max"
                                       value="<?php echo esc_attr($merchant['transit_time_max']); ?>"
                                       placeholder="<?php echo esc_attr(tamrank__('merch_max', 'max')); ?>"
                                       aria-label="<?php echo esc_attr(tamrank__('merch_transit_max_aria', 'Delivery time, longest')); ?>">
                            </div>
                            <span class="tr-lb-help"><?php tamrank_e('merch_transit_help', 'From carrier to doorstep. Both fields are needed, otherwise the delivery time stays out.'); ?></span>
                        </div>
                    </div>
                </div>
            </div>

            <!-- ── Returns ── -->
            <div class="tr-merch-section">
                <div class="tr-lb-switch">
                    <button type="button" class="tr-lb-track<?php echo $returns_on ? ' is-on' : ''; ?>"
                            id="tr-merch-return-toggle" role="switch"
                            aria-checked="<?php echo $returns_on ? 'true' : 'false'; ?>"
                            aria-label="<?php echo esc_attr(tamrank__('merch_return_title', 'Send your return policy')); ?>"></button>
                    <div class="tr-lb-switch-txt">
                        <strong><?php tamrank_e('merch_return_title', 'Send your return policy'); ?></strong>
                        <span><?php tamrank_e('merch_return_sub', 'WooCommerce does not store a return policy, so this is your own statement. It stays switched off until you fill it in.'); ?></span>
                    </div>
                </div>

                <div class="tr-form-grid" id="tr-merch-return-fields" style="<?php echo $returns_on ? '' : 'display:none;'; ?>">
                    <div class="tr-form-grid-2col">
                        <div class="tr-form-field">
                            <label class="tr-form-label" for="tr-merch-return-country"><?php tamrank_e('merch_return_country_label', 'Country the policy applies to'); ?> <span class="tr-required">*</span></label>
                            <?php $this->render_country_select($merchant['return_country'], 'tr_merch_return_country', 'tr-merch-return-country'); ?>
                        </div>
                        <div class="tr-form-field">
                            <label class="tr-form-label" for="tr-merch-return-category"><?php tamrank_e('merch_return_category_label', 'Return window'); ?> <span class="tr-required">*</span></label>
                            <select class="tr-form-input" id="tr-merch-return-category">
                                <option value=""><?php tamrank_e('lb_not_specified', '— not specified —'); ?></option>
                                <?php foreach ($categories as $value => $label) : ?>
                                    <option value="<?php echo esc_attr($value); ?>" <?php selected($merchant['return_category'], $value); ?>><?php echo esc_html($label); ?></option>
                                <?php endforeach; ?>
                            </select>
                        </div>
                    </div>

                    <div class="tr-form-grid-2col">
                        <div class="tr-form-field" id="tr-merch-days-field" style="<?php echo $finite ? '' : 'display:none;'; ?>">
                            <label class="tr-form-label" for="tr-merch-return-days"><?php tamrank_e('merch_return_days_label', 'Number of days'); ?> <span class="tr-required">*</span></label>
                            <input type="number" min="1" max="365" class="tr-form-input" id="tr-merch-return-days"
                                   value="<?php echo esc_attr($merchant['return_days']); ?>" placeholder="14">
                            <span class="tr-lb-help"><?php tamrank_e('merch_return_days_help', 'Counted from delivery. Without this number the whole return policy stays out of the markup.'); ?></span>
                        </div>
                        <div class="tr-form-field">
                            <label class="tr-form-label" for="tr-merch-return-method"><?php tamrank_e('merch_return_method_label', 'How customers return'); ?></label>
                            <select class="tr-form-input" id="tr-merch-return-method">
                                <option value=""><?php tamrank_e('lb_not_specified', '— not specified —'); ?></option>
                                <?php foreach ($methods as $value => $label) : ?>
                                    <option value="<?php echo esc_attr($value); ?>" <?php selected($merchant['return_method'], $value); ?>><?php echo esc_html($label); ?></option>
                                <?php endforeach; ?>
                            </select>
                        </div>
                    </div>

                    <div class="tr-form-grid-2col">
                        <div class="tr-form-field">
                            <label class="tr-form-label" for="tr-merch-return-fees"><?php tamrank_e('merch_return_fees_label', 'Return costs'); ?></label>
                            <select class="tr-form-input" id="tr-merch-return-fees">
                                <option value=""><?php tamrank_e('lb_not_specified', '— not specified —'); ?></option>
                                <?php foreach ($fees as $value => $label) : ?>
                                    <option value="<?php echo esc_attr($value); ?>" <?php selected($merchant['return_fees'], $value); ?>><?php echo esc_html($label); ?></option>
                                <?php endforeach; ?>
                            </select>
                        </div>
                        <div class="tr-form-field" id="tr-merch-fee-field" style="<?php echo $fixed_fee ? '' : 'display:none;'; ?>">
                            <label class="tr-form-label" for="tr-merch-return-fee-amount"><?php tamrank_e('merch_return_fee_label', 'Return fee'); ?> <span class="tr-required">*</span></label>
                            <input type="text" inputmode="decimal" class="tr-form-input" id="tr-merch-return-fee-amount"
                                   value="<?php echo esc_attr($merchant['return_fee_amount']); ?>" placeholder="4.95">
                            <span class="tr-lb-help"><?php tamrank_e('merch_return_fee_help', 'Without an amount the return costs stay out of the markup.'); ?></span>
                        </div>
                    </div>
                </div>
            </div>

            <!-- ── Item condition ── -->
            <div class="tr-merch-section">
                <div class="tr-form-field">
                    <label class="tr-form-label" for="tr-merch-condition"><?php tamrank_e('merch_condition_label', 'Condition of your products'); ?></label>
                    <select class="tr-form-input" id="tr-merch-condition">
                        <option value=""><?php tamrank_e('merch_condition_none', 'Do not state a condition'); ?></option>
                        <?php foreach ($conditions as $value => $label) : ?>
                            <option value="<?php echo esc_attr($value); ?>" <?php selected($merchant['item_condition'], $value); ?>><?php echo esc_html($label); ?></option>
                        <?php endforeach; ?>
                    </select>
                    <span class="tr-lb-help"><?php tamrank_e('merch_condition_help', 'WooCommerce has no field for this, so nothing is stated until you choose. Your choice applies to every product, so leave it unstated if you sell new and second-hand side by side.'); ?></span>
                </div>
            </div>
        </div>
        <?php
    }

    /**
     * Country dropdown. Keeps an unrecognised stored value selectable and
     * flagged rather than silently rewriting it — the user gets to see that
     * addressCountry is not shipping.
     *
     * @param string $current
     */
    private function render_country_select($current, $name = 'tr_address_country', $id = 'tr-address-country')
    {
        $countries = array(
            'NL' => tamrank__('country_nl', 'Netherlands'),
            'BE' => tamrank__('country_be', 'Belgium'),
            'DE' => tamrank__('country_de', 'Germany'),
            'FR' => tamrank__('country_fr', 'France'),
            'GB' => tamrank__('country_gb', 'United Kingdom'),
            'IE' => tamrank__('country_ie', 'Ireland'),
            'US' => tamrank__('country_us', 'United States'),
            'ES' => tamrank__('country_es', 'Spain'),
            'IT' => tamrank__('country_it', 'Italy'),
            'PT' => tamrank__('country_pt', 'Portugal'),
            'AT' => tamrank__('country_at', 'Austria'),
            'CH' => tamrank__('country_ch', 'Switzerland'),
            'LU' => tamrank__('country_lu', 'Luxembourg'),
            'DK' => tamrank__('country_dk', 'Denmark'),
            'SE' => tamrank__('country_se', 'Sweden'),
            'NO' => tamrank__('country_no', 'Norway'),
            'PL' => tamrank__('country_pl', 'Poland'),
        );

        $current      = (string) $current;
        $normalized   = TAMRANK_Schema_Settings::normalize_country($current);
        $unrecognised = ($current !== '' && $normalized === '');
        ?>
        <select class="tr-form-input" name="<?php echo esc_attr($name); ?>" id="<?php echo esc_attr($id); ?>">
            <option value="" <?php selected($current, ''); ?>><?php tamrank_e('lb_not_specified', '— not specified —'); ?></option>
            <?php foreach ($countries as $code => $label) : ?>
                <option value="<?php echo esc_attr($code); ?>" <?php selected($normalized, $code); ?>>
                    <?php echo esc_html($label . ' (' . $code . ')'); ?>
                </option>
            <?php endforeach; ?>
            <?php if ($unrecognised) : ?>
                <option value="" selected><?php echo esc_html(sprintf(
                    /* translators: %s is the unrecognised country value stored in the settings. */
                    tamrank__('lb_country_unrecognised', 'Unrecognised: "%s" — pick a country, otherwise addressCountry is left out'),
                    $current
                )); ?></option>
            <?php endif; ?>
        </select>
        <?php if ($unrecognised) : ?>
            <span class="tr-lb-help tr-lb-help--warn"><?php tamrank_e('lb_country_unrecognised_help', 'This value is not an ISO country code, so it is not shipped. Pick the right country to fix it.'); ?></span>
        <?php endif; ?>
        <?php
    }

    /**
     * Sections 3 (opening hours), 4 (online profiles) and 5 (advanced).
     * All LocalBusiness-only; Organization and Custom never see them.
     *
     * @param array $location
     */
    private function render_lb_sections_3_to_5($location)
    {
        $saved     = $this->settings->get_settings();
        $profiles  = TAMRANK_Schema_Settings::filter_profiles($saved['social_profiles']);
        $day_names = array(
            'monday'    => tamrank__('day_monday', 'Monday'),
            'tuesday'   => tamrank__('day_tuesday', 'Tuesday'),
            'wednesday' => tamrank__('day_wednesday', 'Wednesday'),
            'thursday'  => tamrank__('day_thursday', 'Thursday'),
            'friday'    => tamrank__('day_friday', 'Friday'),
            'saturday'  => tamrank__('day_saturday', 'Saturday'),
            'sunday'    => tamrank__('day_sunday', 'Sunday'),
        );
        ?>
        <!-- ── Section 3 · accordion ── -->
        <div class="tr-lb-section tr-lb-acc tr-lb-only" data-lb-section="3" id="tr-lb-sec3">
            <button type="button" class="tr-lb-shead tr-lb-shead--btn" data-lb-toggle="tr-lb-sec3">
                <span class="tr-lb-caret" aria-hidden="true">▶</span>
                <div>
                    <span class="tr-lb-kicker"><?php tamrank_e('lb_section_3_kicker', 'Step 3'); ?></span>
                    <h3 class="tr-lb-stitle"><?php tamrank_e('lb_section_3_title', 'Opening hours'); ?></h3>
                </div>
                <span class="tr-lb-sum" id="tr-lb-sum3"></span>
            </button>
            <div class="tr-lb-sbody">
                <p class="tr-lb-lead"><?php tamrank_e('lb_section_3_lead', 'One block per day. Google shows these times in the local rich result. Closed for lunch? Enter your first opening and last closing time.'); ?></p>
                <div class="tr-lb-quick">
                    <button type="button" class="tr-btn tr-btn--secondary tr-btn--sm" id="tr-lb-copy-mon-week"><?php tamrank_e('lb_copy_monday_week', 'Copy Monday to Mon–Fri'); ?></button>
                    <button type="button" class="tr-btn tr-btn--ghost tr-btn--sm" id="tr-lb-copy-mon-all"><?php tamrank_e('lb_copy_monday_all', '…to every day'); ?></button>
                </div>
                <?php // Terugkoppeling van de kopieerknoppen: wat er gebeurde, of waarom er niets gebeurde. ?>
                <div class="tr-lb-note tr-lb-note--warn" id="tr-lb-copy-msg" role="status" aria-live="polite" style="display:none;">
                    <span aria-hidden="true" id="tr-lb-copy-msg-icon">⚠</span>
                    <span id="tr-lb-copy-msg-text"></span>
                </div>
                <div id="tr-lb-hours">
                    <?php foreach (TAMRANK_Schema_Settings::get_day_keys() as $day) :
                        $row = $location['opening_hours'][$day]; ?>
                        <div class="tr-lb-hrow<?php echo !empty($row['closed']) ? ' is-closed' : ''; ?>" data-day="<?php echo esc_attr($day); ?>">
                            <span class="tr-lb-hday"><?php echo esc_html($day_names[$day]); ?></span>
                            <input type="time" class="tr-form-input tr-lb-htime tr-lb-opens" value="<?php echo esc_attr($row['opens']); ?>"
                                   aria-label="<?php echo esc_attr($day_names[$day] . ' — ' . tamrank__('lb_opens', 'opens')); ?>">
                            <span class="tr-lb-hsep tr-lb-htime"><?php tamrank_e('lb_until', 'until'); ?></span>
                            <input type="time" class="tr-form-input tr-lb-htime tr-lb-closes" value="<?php echo esc_attr($row['closes']); ?>"
                                   aria-label="<?php echo esc_attr($day_names[$day] . ' — ' . tamrank__('lb_closes', 'closes')); ?>">
                            <label class="tr-lb-hclosed">
                                <input type="checkbox" class="tr-check tr-lb-cb-closed" <?php checked(!empty($row['closed'])); ?>>
                                <?php tamrank_e('lb_closed', 'closed'); ?>
                            </label>
                        </div>
                    <?php endforeach; ?>
                </div>
            </div>
        </div>

        <!-- ── Section 4 · accordion ── -->
        <div class="tr-lb-section tr-lb-acc tr-lb-only" data-lb-section="4" id="tr-lb-sec4">
            <button type="button" class="tr-lb-shead tr-lb-shead--btn" data-lb-toggle="tr-lb-sec4">
                <span class="tr-lb-caret" aria-hidden="true">▶</span>
                <div>
                    <span class="tr-lb-kicker"><?php tamrank_e('lb_section_4_kicker', 'Step 4'); ?></span>
                    <h3 class="tr-lb-stitle"><?php tamrank_e('lb_section_4_title', 'Online profiles'); ?></h3>
                </div>
                <span class="tr-lb-sum" id="tr-lb-sum4"></span>
            </button>
            <div class="tr-lb-sbody">
                <p class="tr-lb-lead"><?php tamrank_e('lb_section_4_lead', 'Your Google Business Profile and socials, shipped as sameAs[]. This is how Google links your website to the profiles it already knows.'); ?></p>
                <div id="tr-lb-profiles">
                    <?php foreach ($profiles as $profile) : ?>
                        <div class="tr-lb-profile-row">
                            <input type="url" class="tr-form-input tr-lb-profile" value="<?php echo esc_attr($profile); ?>" placeholder="https://…">
                            <button type="button" class="tr-lb-icon-btn tr-lb-profile-remove"
                                    aria-label="<?php echo esc_attr(tamrank__('delete', 'Delete')); ?>">×</button>
                        </div>
                    <?php endforeach; ?>
                </div>
                <button type="button" class="tr-btn tr-btn--ghost tr-btn--sm" id="tr-lb-add-profile">+ <?php tamrank_e('lb_add_profile', 'Add profile'); ?></button>
            </div>
        </div>

        <!-- ── Section 5 · behind Advanced ── -->
        <div class="tr-lb-section tr-lb-acc tr-lb-adv tr-lb-only" data-lb-section="5" id="tr-lb-sec5">
            <button type="button" class="tr-lb-shead tr-lb-shead--btn" data-lb-toggle="tr-lb-sec5">
                <span class="tr-lb-caret" aria-hidden="true">▶</span>
                <div>
                    <span class="tr-lb-kicker"><?php tamrank_e('lb_section_5_kicker', 'Step 5'); ?></span>
                    <h3 class="tr-lb-stitle"><?php tamrank_e('advanced', 'Advanced'); ?></h3>
                </div>
                <span class="tr-lb-sum"><span class="tr-lb-sum-empty"><?php tamrank_e('lb_section_5_sum', 'Low-impact fields'); ?></span></span>
            </button>
            <div class="tr-lb-sbody">
                <p class="tr-lb-lead"><?php tamrank_e('lb_section_5_lead', 'Everything here is optional — only fill in what you know for sure. Leaving a field empty beats guessing.'); ?></p>
                <div class="tr-form-grid">
                    <div class="tr-form-grid-2col">
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('lb_latitude_label', 'Latitude'); ?></label>
                            <input type="text" class="tr-form-input" name="tr_lb_lat" id="tr-lb-lat"
                                   value="<?php echo esc_attr($location['geo']['lat']); ?>" placeholder="52.295">
                        </div>
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('lb_longitude_label', 'Longitude'); ?></label>
                            <input type="text" class="tr-form-input" name="tr_lb_lng" id="tr-lb-lng"
                                   value="<?php echo esc_attr($location['geo']['lng']); ?>" placeholder="5.162">
                        </div>
                    </div>
                    <span class="tr-lb-help"><?php tamrank_e('lb_geo_help', 'Right-click your building in Google Maps and paste the two numbers from the top of the menu. No lookup button — that would need an external service with an API key.'); ?></span>

                    <div class="tr-form-grid-2col">
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('lb_price_range_label', 'Price range'); ?></label>
                            <select class="tr-form-input" name="tr_lb_price" id="tr-lb-price">
                                <option value="" <?php selected($location['price_range'], ''); ?>><?php tamrank_e('lb_not_specified', '— not specified —'); ?></option>
                                <option value="$" <?php selected($location['price_range'], '$'); ?>>$ — <?php tamrank_e('lb_price_budget', 'budget'); ?></option>
                                <option value="$$" <?php selected($location['price_range'], '$$'); ?>>$$ — <?php tamrank_e('lb_price_average', 'average'); ?></option>
                                <option value="$$$" <?php selected($location['price_range'], '$$$'); ?>>$$$ — <?php tamrank_e('lb_price_above', 'above average'); ?></option>
                                <option value="$$$$" <?php selected($location['price_range'], '$$$$'); ?>>$$$$ — <?php tamrank_e('lb_price_luxury', 'luxury'); ?></option>
                            </select>
                            <span class="tr-lb-help"><?php tamrank_e('lb_price_help', 'Google\'s own examples use dollar signs, whatever your country or currency.'); ?></span>
                        </div>
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('lb_founding_label', 'Founding date'); ?></label>
                            <input type="text" class="tr-form-input" name="tr_lb_founding" id="tr-lb-founding"
                                   value="<?php echo esc_attr($location['founding_date']); ?>" placeholder="yyyy-mm-dd">
                        </div>
                    </div>

                    <div class="tr-form-field">
                        <label class="tr-form-label"><?php tamrank_e('lb_images_label', 'Extra images'); ?></label>
                        <div class="tr-lb-media-row">
                            <input type="text" class="tr-form-input" id="tr-lb-images-label" readonly
                                   value="<?php echo esc_attr($this->describe_attachments($location['images'])); ?>"
                                   placeholder="<?php echo esc_attr(tamrank__('lb_images_none', 'No images selected yet')); ?>">
                            <input type="hidden" id="tr-lb-images" value="<?php echo esc_attr(implode(',', $location['images'])); ?>">
                            <button type="button" class="tr-btn tr-btn--ghost" id="tr-lb-pick-images"><?php tamrank_e('lb_pick_media', 'Choose media'); ?></button>
                            <button type="button" class="tr-btn tr-btn--ghost" id="tr-lb-clear-images"><?php tamrank_e('lb_clear', 'Clear'); ?></button>
                        </div>
                        <span class="tr-lb-help"><?php tamrank_e('lb_images_help', 'Shipped as image[] — photos of the premises work best. Stored as media IDs, so they survive a domain change.'); ?></span>
                    </div>

                    <div class="tr-form-grid-2col">
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('lb_vat_label', 'VAT ID'); ?></label>
                            <input type="text" class="tr-form-input" name="tr_lb_vat" id="tr-lb-vat" value="<?php echo esc_attr($location['vat_id']); ?>">
                        </div>
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('lb_tax_label', 'Tax ID'); ?></label>
                            <input type="text" class="tr-form-input" name="tr_lb_tax" id="tr-lb-tax" value="<?php echo esc_attr($location['tax_id']); ?>">
                        </div>
                    </div>

                    <div class="tr-form-grid-3col">
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('lb_cp_phone_label', 'Contact point — phone'); ?></label>
                            <input type="tel" class="tr-form-input" name="tr_lb_cp_tel" id="tr-lb-cp-tel" value="<?php echo esc_attr($location['contact_point']['telephone']); ?>">
                        </div>
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('lb_cp_type_label', 'Contact type'); ?></label>
                            <select class="tr-form-input" name="tr_lb_cp_type" id="tr-lb-cp-type">
                                <option value="" <?php selected($location['contact_point']['contact_type'], ''); ?>><?php tamrank_e('lb_not_specified', '— not specified —'); ?></option>
                                <option value="customer service" <?php selected($location['contact_point']['contact_type'], 'customer service'); ?>><?php tamrank_e('lb_cp_customer_service', 'Customer service'); ?></option>
                                <option value="sales" <?php selected($location['contact_point']['contact_type'], 'sales'); ?>><?php tamrank_e('lb_cp_sales', 'Sales'); ?></option>
                                <option value="technical support" <?php selected($location['contact_point']['contact_type'], 'technical support'); ?>><?php tamrank_e('lb_cp_support', 'Technical support'); ?></option>
                                <option value="reservations" <?php selected($location['contact_point']['contact_type'], 'reservations'); ?>><?php tamrank_e('lb_cp_reservations', 'Reservations'); ?></option>
                            </select>
                        </div>
                        <div class="tr-form-field">
                            <label class="tr-form-label"><?php tamrank_e('lb_payment_label', 'Payment accepted'); ?></label>
                            <input type="text" class="tr-form-input" name="tr_lb_payment" id="tr-lb-payment"
                                   value="<?php echo esc_attr(implode(', ', $location['payment_accepted'])); ?>">
                        </div>
                    </div>

                    <div class="tr-lb-note tr-lb-note--info">
                        <span aria-hidden="true">ℹ</span>
                        <span><b><?php tamrank_e('lb_no_reviews_title', 'Deliberately not in this form'); ?></b>
                        <?php tamrank_e('lb_no_reviews_text', 'aggregateRating and review — only allowed with real reviews on your own site; filling them in yourself risks a manual action from Google.'); ?></span>
                    </div>
                </div>
            </div>
        </div>
        <?php
    }

    /**
     * Human-readable summary of the picked attachments.
     *
     * @param array $ids
     * @return string
     */
    private function describe_attachments($ids)
    {
        $names = array();
        foreach ((array) $ids as $id) {
            $file = get_attached_file((int) $id);
            if ($file) {
                $names[] = basename($file);
            }
        }

        return implode(', ', $names);
    }

    /**
     * The sticky right-hand panel: coverage meter, live JSON, output location
     * and the Rich Results link.
     *
     * @param array $lb
     */
    private function render_lb_panel($lb)
    {
        $pages = get_posts(array(
            'post_type'   => 'page',
            'post_status' => 'publish',
            'numberposts' => 200,
            'orderby'     => 'title',
            'order'       => 'ASC',
        ));

        // A suggestion, never a silent pick: a page called "Kontakt" or
        // "Call us" would slip past any slug trick, so the user confirms.
        $suggested = 0;
        foreach ($pages as $page) {
            if (strpos($page->post_name, 'contact') !== false || stripos($page->post_title, 'contact') !== false) {
                $suggested = $page->ID;
                break;
            }
        }
        $selected_page = $lb['contact_page_id'] > 0 ? $lb['contact_page_id'] : $suggested;
        ?>
        <aside class="tr-lb-panel tr-lb-only">

            <div class="tr-lb-pcard">
                <div class="tr-lb-pcard-h">
                    <h3><?php tamrank_e('lb_coverage_title', 'Google coverage'); ?></h3>
                    <span class="tr-lb-meter"><i id="tr-lb-cov-bar"></i></span>
                    <span class="tr-lb-cov-n" id="tr-lb-cov-txt">0/9</span>
                </div>
                <div class="tr-lb-pcard-b">
                    <div class="tr-lb-chk-t"><?php tamrank_e('lb_required', 'Required'); ?></div>
                    <div class="tr-lb-chk" id="tr-lb-chk-req"></div>
                    <div class="tr-lb-chk-t"><?php tamrank_e('lb_recommended', 'Recommended'); ?></div>
                    <div class="tr-lb-chk" id="tr-lb-chk-rec"></div>
                </div>
            </div>

            <div class="tr-lb-pcard">
                <div class="tr-lb-pcard-h">
                    <h3><?php tamrank_e('lb_json_title', 'What Google reads'); ?></h3>
                    <button type="button" class="tr-btn tr-btn--ghost tr-btn--sm" id="tr-lb-json-all" style="margin-left:auto;"><?php tamrank_e('lb_show_all', 'Show all'); ?></button>
                    <span class="tr-lb-live"><?php tamrank_e('lb_live', 'live'); ?></span>
                </div>
                <pre class="tr-lb-json" id="tr-lb-json"></pre>
                <div class="tr-lb-json-foot"><?php tamrank_e('lb_json_foot', 'This is the LocalBusiness node, built by the plugin itself. In the real output it sits in one @graph next to the WebSite node, which points at it with publisher.'); ?></div>
            </div>

            <div class="tr-lb-pcard" id="tr-lb-loc-card">
                <div class="tr-lb-pcard-h"><h3><?php tamrank_e('lb_output_title', 'Where do we place this?'); ?></h3></div>
                <div class="tr-lb-pcard-b">
                    <div class="tr-lb-radios">
                        <label class="tr-lb-radio<?php echo $lb['output_location'] === 'homepage' ? ' is-on' : ''; ?>">
                            <input type="radio" name="tr_lb_output" value="homepage" <?php checked($lb['output_location'], 'homepage'); ?>>
                            <span><b><?php tamrank_e('lb_output_homepage', 'Homepage only'); ?> <span class="tr-lb-tag-def"><?php tamrank_e('lb_recommended_tag', 'recommended'); ?></span></b>
                            <span><?php tamrank_e('lb_output_homepage_desc', 'The page that describes your business.'); ?></span></span>
                        </label>
                        <label class="tr-lb-radio<?php echo $lb['output_location'] === 'homepage_contact' ? ' is-on' : ''; ?>">
                            <input type="radio" name="tr_lb_output" value="homepage_contact" <?php checked($lb['output_location'], 'homepage_contact'); ?>>
                            <span><b><?php tamrank_e('lb_output_contact', 'Homepage + contact page'); ?></b>
                            <span><?php tamrank_e('lb_output_contact_desc', 'If your details are listed there too.'); ?></span></span>
                        </label>
                        <label class="tr-lb-radio<?php echo $lb['output_location'] === 'all' ? ' is-on' : ''; ?>">
                            <input type="radio" name="tr_lb_output" value="all" <?php checked($lb['output_location'], 'all'); ?>>
                            <span><b><?php tamrank_e('lb_output_all', 'All pages'); ?></b>
                            <span><?php tamrank_e('lb_output_all_desc', 'Current behaviour. According to Google it adds nothing.'); ?></span></span>
                        </label>
                    </div>
                    <div class="tr-form-field" id="tr-lb-contact-pick"
                         style="margin-top:12px;<?php echo $lb['output_location'] === 'homepage_contact' ? '' : 'display:none;'; ?>">
                        <label class="tr-form-label"><?php tamrank_e('lb_which_page', 'Which page is that?'); ?></label>
                        <select class="tr-form-input" id="tr-lb-contact-page">
                            <option value="0"><?php tamrank_e('lb_pick_page', '— pick a page —'); ?></option>
                            <?php foreach ($pages as $page) : ?>
                                <option value="<?php echo esc_attr($page->ID); ?>" <?php selected($selected_page, $page->ID); ?>>
                                    <?php echo esc_html($page->post_title . ' · /' . $page->post_name . '/'
                                        . ($page->ID === $suggested ? '  (' . tamrank__('lb_suggestion', 'suggestion') . ')' : '')); ?>
                                </option>
                            <?php endforeach; ?>
                        </select>
                        <span class="tr-lb-help"><?php tamrank_e('lb_pick_page_help', 'We suggest, but never pick silently: a page called "Kontakt" or "Call us" would slip past a slug trick.'); ?></span>
                    </div>
                    <div class="tr-lb-note tr-lb-note--info" style="margin-top:12px;">
                        <span aria-hidden="true">ℹ</span>
                        <span><?php tamrank_e('lb_same_id_note', 'The same @id everywhere'); ?>
                        (<code id="tr-lb-id-preview"><?php echo esc_html($this->settings->get_localbusiness_id()); ?></code>) —
                        <?php tamrank_e('lb_same_id_note_2', 'Google sees one business, not several.'); ?></span>
                    </div>
                </div>
            </div>

            <div class="tr-lb-pcard">
                <div class="tr-lb-pcard-h"><h3><?php tamrank_e('lb_check_google_title', 'Check with Google'); ?></h3></div>
                <div class="tr-lb-pcard-b">
                    <div class="tr-lb-note tr-lb-note--warn" id="tr-lb-test-dirty" style="display:none;margin-bottom:12px;">
                        <span aria-hidden="true">⚠</span>
                        <span><b><?php tamrank_e('lb_save_first', 'Save first'); ?></b>
                        <?php tamrank_e('lb_save_first_text', 'You have unsaved changes. The test reads your site, not this form — without saving you are testing the old version.'); ?></span>
                    </div>
                    <a class="tr-btn tr-btn--secondary tr-lb-btn-block"
                       href="<?php echo esc_url('https://search.google.com/test/rich-results?url=' . rawurlencode(home_url('/'))); ?>"
                       target="_blank" rel="noopener"><?php tamrank_e('lb_test_rich_results', 'Test in Rich Results'); ?> ↗</a>
                    <p class="tr-lb-help" style="margin-top:12px;"><?php tamrank_e('lb_test_help', 'The test fetches your page as it is live right now — so the last saved version. And it only works on a publicly reachable site: Google cannot reach a local site or one behind a password. Copy the JSON above into the "Code" tab in that case.'); ?></p>
                </div>
            </div>

        </aside>
        <?php
    }

    /* ================================================================
     *  FORM HANDLERS (non-AJAX fallback)
     * ================================================================ */

    /**
     * Handle template save form submission
     */
    public function handle_save_template()
    {
        if (!isset($_POST['tamrank_schema_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['tamrank_schema_nonce'])), 'tamrank_schema_template')) {
            wp_die(esc_html(tamrank__('error_security_token', 'Invalid security token. Please refresh the page.')));
        }
        if (!current_user_can('manage_options')) {
            wp_die(esc_html(tamrank__('error_permission_denied', 'You do not have permission for this action.')));
        }

        $template_id = isset($_POST['template_id']) ? sanitize_text_field(wp_unslash($_POST['template_id'])) : null;

        $template_data = array(
            'name' => sanitize_text_field(wp_unslash($_POST['template_name'] ?? '')),
            'schema_type' => sanitize_text_field(wp_unslash($_POST['schema_type'] ?? '')),
            'post_types' => isset($_POST['post_types']) ? array_map('sanitize_text_field', wp_unslash((array) $_POST['post_types'])) : array(),
            'field_mappings' => isset($_POST['field_mappings']) ? map_deep(wp_unslash($_POST['field_mappings']), 'sanitize_text_field') : array(),
            'custom_jsonld_enabled' => isset($_POST['custom_jsonld_enabled']),
            'custom_jsonld_code' => isset($_POST['custom_jsonld_code']) ? wp_unslash($_POST['custom_jsonld_code']) : '' // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- raw JSON-LD validated below via tamrank_validate_jsonld() before storage, then stored verbatim and escaped on output (wp_json_encode JSON_HEX_*); text sanitization would corrupt the JSON.
        );

        // Validate-on-store: reject malformed custom JSON-LD before it is persisted.
        if (!empty($template_data['custom_jsonld_enabled']) && trim((string) $template_data['custom_jsonld_code']) !== '') {
            $jsonld_check = tamrank_validate_jsonld($template_data['custom_jsonld_code']);
            if (empty($jsonld_check['valid'])) {
                $jsonld_error = !empty($jsonld_check['errors']) ? implode(' ', $jsonld_check['errors']) : tamrank__('error_invalid_jsonld', 'The custom JSON-LD is not valid JSON.');
                wp_safe_redirect(admin_url('admin.php?page=tam-rank-schema&error=' . urlencode($jsonld_error)));
                exit;
            }
        }

        if ($template_id) {
            $result = $this->templates->update_template($template_id, $template_data);
        } else {
            $result = $this->templates->create_template($template_data);
        }

        if (is_wp_error($result)) {
            wp_safe_redirect(admin_url('admin.php?page=tam-rank-schema&error=' . urlencode($result->get_error_message())));
        } else {
            wp_safe_redirect(admin_url('admin.php?page=tam-rank-schema&message=template_saved'));
        }
        exit;
    }

    /**
     * Handle test schema form submission
     */
    public function handle_test_schema()
    {
        if (!isset($_POST['tamrank_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['tamrank_nonce'])), 'tamrank_test_schema')) {
            wp_die(esc_html(tamrank__('error_security_token', 'Invalid security token. Please refresh the page.')));
        }
        if (!current_user_can('manage_options')) {
            wp_die(esc_html(tamrank__('error_permission_denied', 'You do not have permission for this action.')));
        }

        $test_url = isset($_POST['test_url']) ? esc_url_raw(wp_unslash($_POST['test_url'])) : '';
        if (empty($test_url)) {
            wp_safe_redirect(admin_url('admin.php?page=tam-rank-schema&error=' . urlencode(tamrank__('fill_url', 'Enter a URL'))));
            exit;
        }

        $post_id = url_to_postid($test_url);
        $result = array('url' => $test_url, 'post_id' => $post_id, 'error' => null);

        if ($post_id) {
            $post = get_post($post_id);

            if ($this->detector_manager) {
                $detectie_result = $this->detector_manager->voer_detectie_uit($post);
                if ($detectie_result && $detectie_result->is_geldig()) {
                    $extra_schemas = $detectie_result->krijg_extra_schemas();
                    if (tamrank_post_type_has_breadcrumb_schema($post->post_type)) {
                        if (!in_array('BreadcrumbList', $extra_schemas, true)) {
                            $extra_schemas[] = 'BreadcrumbList';
                        }
                    }
                    $result['detectie'] = array(
                        'hoofd_schema' => $detectie_result->krijg_hoofd_schema(),
                        'extra_schemas' => $extra_schemas,
                        'betrouwbaarheid' => $detectie_result->krijg_betrouwbaarheid()
                    );
                }
            }

            // $test_url is constrained to this site's own origin (url_to_postid() bails on
            // off-host URLs above), so this is a same-origin fetch. Verify TLS in production;
            // relax only on local WP_DEBUG dev where self-signed certs are common.
            $response = wp_remote_get($test_url, array('timeout' => 10, 'sslverify' => ! ( defined( 'WP_DEBUG' ) && WP_DEBUG )));
            if (!is_wp_error($response)) {
                $body = wp_remote_retrieve_body($response);
                if (preg_match_all('/<script[^>]*type=["\']application\/ld\+json["\'][^>]*>(.*?)<\/script>/is', $body, $matches)) {
                    $schemas = array();
                    $gevonden_types = array();
                    foreach ($matches[1] as $json) {
                        $decoded = json_decode(trim($json), true);
                        if ($decoded) {
                            $schemas[] = $decoded;
                            $this->extract_schema_types($decoded, $gevonden_types);
                        }
                    }
                    if (!empty($schemas)) {
                        $result['schema_output'] = wp_json_encode($schemas, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
                    }
                    if (!empty($gevonden_types)) {
                        $result['gevonden_types'] = array_unique($gevonden_types);
                    }
                }
            }
        } else {
            $result['error'] = __('Could not find a post for this URL.', 'tam-rank');
        }

        set_transient('tamrank_test_schema_result_' . get_current_user_id(), $result, 60);
        wp_safe_redirect(admin_url('admin.php?page=tam-rank-schema&tested=1'));
        exit;
    }

    /**
     * Extract @type values from schema data recursively
     */
    private function extract_schema_types($data, &$types)
    {
        if (!is_array($data)) return;

        if (isset($data['@type'])) {
            $type = $data['@type'];
            if (is_array($type)) {
                foreach ($type as $t) {
                    if (is_string($t) && !in_array($t, $types)) $types[] = $t;
                }
            } elseif (is_string($type) && !in_array($type, $types)) {
                $types[] = $type;
            }
        }

        foreach ($data as $key => $value) {
            if (is_array($value)) {
                if ($key === '@graph') {
                    foreach ($value as $item) $this->extract_schema_types($item, $types);
                } else {
                    $this->extract_schema_types($value, $types);
                }
            }
        }
    }

    /* ================================================================
     *  GETTERS
     * ================================================================ */

    public function get_settings()
    {
        return $this->settings;
    }

    public function get_templates()
    {
        return $this->templates;
    }

    public function get_renderer()
    {
        return $this->renderer;
    }

    public function get_audit()
    {
        return $this->audit;
    }
}

/**
 * Standalone function to render schema admin page
 */
function tamrank_schema_render_admin_page()
{
    $manager = tamrank_get_schema_manager();
    if ($manager) {
        $manager->render_admin_page();
    } else {
        echo '<div class="wrap"><h1>Schema Builder</h1>';
        echo '<div class="notice notice-error"><p>' . esc_html__( 'The Schema Manager could not be loaded.', 'tam-rank' ) . '</p></div></div>';
    }
}
