<?php
defined( 'ABSPATH' ) || exit;

/**
 * Jalpi Page Builder Settings Page.
 *
 * Registers a "Jalpi Page Builder → Settings" submenu page and exposes five
 * sections (General, Layout, Global Design, Performance, Breakpoints) through
 * the WordPress Settings API.  All values are available via Jalpi_Settings::get().
 *
 * Extensible via:
 *   - jalpi_settings_tabs        filter — add / reorder tabs
 *   - jalpi_settings_defaults    filter — override default values
 *   - jalpi_settings_font_options filter — alter the font select list
 *   - jalpi_selectable_post_types filter — alter which post types appear
 *   - jalpi_sanitize_*_settings  filters — post-sanitize per section
 *   - jalpi_global_css_vars      filter — add / override CSS custom properties
 */
class Jalpi_Settings {

    private static ?self $instance = null;

    // One WordPress option key per settings tab.
    const OPT_GENERAL       = 'jalpi_settings_general';
    const OPT_LAYOUT        = 'jalpi_settings_layout';
    const OPT_DESIGN        = 'jalpi_settings_design';
    const OPT_PERFORMANCE   = 'jalpi_settings_performance';
    const OPT_DESIGN_SYSTEM = 'jalpi_settings_design_system';

    const OPT_BREAKPOINTS  = 'jalpi_settings_breakpoints';

    /**
     * Design System token categories: short key (used in globalStyleRef.tokens
     * and CSS var prefixes) => storage key inside the design_system option.
     */
    const TOKEN_CATEGORIES = [
        'color'          => 'colors',
        'fontFamily'     => 'fontFamilies',
        'fontSize'       => 'fontSizes',
        'lineHeight'     => 'lineHeights',
        'letterSpacing'  => 'letterSpacing',
        'borderRadius'   => 'borderRadius',
        'boxShadow'      => 'boxShadows',
        'spacing'        => 'spacing',
        'containerWidth' => 'containerWidths',
        'opacity'        => 'opacity',
    ];

    /** Token category => CSS custom property prefix (--jalpi-{prefix}-{id}). */
    const TOKEN_VAR_PREFIX = [
        'color'          => 'color',
        'fontFamily'     => 'font',
        'fontSize'       => 'fs',
        'lineHeight'     => 'lh',
        'letterSpacing'  => 'ls',
        'borderRadius'   => 'radius',
        'boxShadow'      => 'shadow',
        'spacing'        => 'space',
        'containerWidth' => 'container',
        'opacity'        => 'opacity',
    ];

    private string $page_hook    = '';
    private string $current_tab  = 'general';

    // =========================================================================
    // Bootstrapping
    // =========================================================================

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

    private function __construct() {
        add_action( 'admin_menu',            [ $this, 'add_menu' ] );
        add_action( 'admin_init',            [ $this, 'register_settings' ] );
        add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
        add_action( 'wp_enqueue_scripts',     [ $this, 'output_global_styles' ] );
        add_action( 'wp_enqueue_scripts',     [ $this, 'output_design_system_css' ] );

        add_filter( 'use_block_editor_for_post_type', [ $this, 'maybe_disable_gutenberg' ], 10, 2 );
        // Local by Flywheel routes wp-admin/jalpi-page-builder-settings (without admin.php?page=)
        // to the WordPress frontend router, which returns a 404. Catch it here and redirect.
        add_action( 'template_redirect', [ $this, 'redirect_bad_settings_url' ] );
    }

    // =========================================================================
    // Public static API
    // =========================================================================

    /**
     * Retrieve a single setting value.
     *
     * @param string $section  Tab key: general|layout|design|performance|breakpoints
     * @param string $key      Field key within that section.
     * @param mixed  $default  Returned when the key is absent.
     * @return mixed
     */
    public static function get( string $section, string $key, $default = null ) {
        $option   = 'jalpi_settings_' . $section;
        $defaults = self::defaults()[ $section ] ?? [];
        $saved    = get_option( $option, $defaults );

        if ( ! is_array( $saved ) ) {
            $saved = $defaults;
        }

        if ( array_key_exists( $key, $saved ) ) {
            return $saved[ $key ];
        }

        return $default ?? ( $defaults[ $key ] ?? null );
    }

    /**
     * Retrieve all settings for a section, merged with defaults.
     *
     * @param string $section
     * @return array
     */
    public static function get_section( string $section ): array {
        $option   = 'jalpi_settings_' . $section;
        $defaults = self::defaults()[ $section ] ?? [];
        $saved    = get_option( $option, $defaults );
        return is_array( $saved ) ? wp_parse_args( $saved, $defaults ) : $defaults;
    }

    /**
     * Default values for every setting.
     * Override via the jalpi_settings_defaults filter.
     *
     * @return array
     */
    public static function defaults(): array {
        return apply_filters( 'jalpi_settings_defaults', [
            'general' => [
                'enable_builder'     => '1',
                'enabled_post_types' => [ 'page', 'post' ],
                'disable_gutenberg'  => [],
            ],
            'layout' => [
                'container_width' => '1200px',
                'section_padding' => '80px 0',
                'column_gap'      => '20px',
            ],
            'design' => [
                'primary_color'   => '#6366f1',
                'secondary_color' => '#8b5cf6',
                'body_font'       => '',
                'heading_font'    => '',
                'base_font_size'  => '16',
            ],
            'performance' => [
                'enable_css_generation' => '1',
                'enable_lazy_loading'   => '1',
                'enable_page_cache'     => '',
            ],

            'breakpoints' => [
                'tablet_breakpoint' => '1024',
                'mobile_breakpoint' => '767',
            ],

            'design_system' => self::default_design_system_tokens(),
        ] );
    }

    /**
     * Seed values for a brand-new Design System — sensible starting tokens
     * covering all 9 categories plus empty composite-preset lists.
     * Used both as the register_setting() default and by the activator's
     * one-time seed step (which additionally pulls forward any existing
     * "Global Design" tab values — see Jalpi_Activator::seed_design_system()).
     *
     * @return array
     */
    public static function default_design_system_tokens(): array {
        return [
            'colors'       => [
                [ 'id' => 'primary',   'name' => 'Primary',   'value' => '#6366f1' ],
                [ 'id' => 'secondary', 'name' => 'Secondary', 'value' => '#8b5cf6' ],
                [ 'id' => 'dark',      'name' => 'Dark',      'value' => '#111827' ],
                [ 'id' => 'white',     'name' => 'White',     'value' => '#ffffff' ],
            ],
            'fontFamilies'    => [],
            'fontSizes'       => [
                [ 'id' => 'sm',   'name' => 'Small',   'value' => '14px' ],
                [ 'id' => 'base', 'name' => 'Base',     'value' => '16px' ],
                [ 'id' => 'lg',   'name' => 'Large',   'value' => '20px' ],
            ],
            'lineHeights'     => [
                [ 'id' => 'tight',  'name' => 'Tight',  'value' => '1.2' ],
                [ 'id' => 'normal', 'name' => 'Normal', 'value' => '1.6' ],
            ],
            'letterSpacing'   => [
                [ 'id' => 'normal', 'name' => 'Normal', 'value' => '0' ],
                [ 'id' => 'wide',   'name' => 'Wide',   'value' => '0.02em' ],
            ],
            'borderRadius'    => [
                [ 'id' => 'sm', 'name' => 'Small',  'value' => '4px' ],
                [ 'id' => 'md', 'name' => 'Medium', 'value' => '8px' ],
            ],
            'boxShadows'      => [
                [ 'id' => 'sm', 'name' => 'Small', 'value' => '0 1px 4px rgba(0,0,0,0.1)' ],
            ],
            'spacing'         => [
                [ 'id' => 'xs',  'name' => 'XS',  'value' => '4px' ],
                [ 'id' => 'sm',  'name' => 'SM',  'value' => '8px' ],
                [ 'id' => 'md',  'name' => 'MD',  'value' => '16px' ],
                [ 'id' => 'lg',  'name' => 'LG',  'value' => '32px' ],
                [ 'id' => 'xl',  'name' => 'XL',  'value' => '64px' ],
                [ 'id' => '2xl', 'name' => '2XL', 'value' => '128px' ],
            ],
            'containerWidths' => [
                [ 'id' => 'default', 'name' => 'Default', 'value' => '1200px' ],
            ],
            'opacity'         => [
                [ 'id' => 'subtle',  'name' => 'Subtle',  'value' => '0.6' ],
                [ 'id' => 'overlay', 'name' => 'Overlay', 'value' => '0.5' ],
            ],
            'typography' => [],
            'buttons'    => [],
        ];
    }

    // =========================================================================
    // Admin menu
    // =========================================================================

    public function add_menu(): void {
        // Register as a hidden top-level page so the hookname is always predictable
        // (toplevel_page_jalpi-page-builder-settings) regardless of when the parent menu
        // is registered.  render_page() handles all real access control.
        $this->page_hook = add_menu_page(
            __( 'Jalpi Page Builder Settings', 'jalpi-page-builder' ),
            __( 'Jalpi Page Builder Settings', 'jalpi-page-builder' ),
            'read',
            'jalpi-page-builder-settings',
            [ $this, 'render_page' ]
        );
        // Hide it from the admin sidebar — it's accessed via direct URL only.
        remove_menu_page( 'jalpi-page-builder-settings' );

        // Attach a "Settings" submenu entry under the Jalpi Page Builder menu so users
        // have a visible link.  This is purely cosmetic; URL access is via the
        // top-level page registered above.
        add_submenu_page(
            'jalpi-page-builder-editor',
            __( 'Jalpi Page Builder Settings', 'jalpi-page-builder' ),
            __( 'Settings', 'jalpi-page-builder' ),
            'read',
            'jalpi-page-builder-settings'
            // No callback — the top-level page handles rendering.
        );
    }

    /**
     * Local by Flywheel (and some nginx setups) route wp-admin/jalpi-page-builder-settings
     * (the slug without admin.php?page=) to the WordPress frontend instead of the admin.
     * This results in a frontend 404 and an AbortError in WordPress 6.5's View Transitions.
     * Intercept any 404 whose URI contains the settings slug and redirect to the real URL.
     */
    public function redirect_bad_settings_url(): void {
        if ( ! is_404() ) {
            return;
        }
        $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
        if ( false !== strpos( $uri, 'jalpi-page-builder-settings' ) ) {
            wp_safe_redirect( admin_url( 'admin.php?page=jalpi-page-builder-settings' ), 302 );
            exit;
        }
    }

    // =========================================================================
    // Settings API registration
    // =========================================================================

    public function register_settings(): void {
        $this->register_general();
        $this->register_layout();
        $this->register_design();
        $this->register_performance();
        $this->register_design_system();

        $this->register_breakpoints();
    }

    private function register_general(): void {
        register_setting( 'jalpi_group_general', self::OPT_GENERAL, [
            'sanitize_callback' => [ $this, 'sanitize_general' ],
            'default'           => self::defaults()['general'],
        ] );
        add_settings_section( 'jalpi_sec_general', __( 'Builder Settings', 'jalpi-page-builder' ),
            fn() => $this->section_desc( __( 'Control which post types can use Jalpi Page Builder.', 'jalpi-page-builder' ) ),
            'jalpi_page_general' );
        add_settings_field( 'jalpi_enable_builder',    __( 'Enable Builder',        'jalpi-page-builder' ), [ $this, 'field_enable_builder'    ], 'jalpi_page_general', 'jalpi_sec_general' );
        add_settings_field( 'jalpi_enabled_post_types',__( 'Supported Post Types',  'jalpi-page-builder' ), [ $this, 'field_enabled_post_types' ], 'jalpi_page_general', 'jalpi_sec_general' );
        add_settings_field( 'jalpi_disable_gutenberg', __( 'Disable Gutenberg',     'jalpi-page-builder' ), [ $this, 'field_disable_gutenberg'  ], 'jalpi_page_general', 'jalpi_sec_general' );
    }

    private function register_layout(): void {
        register_setting( 'jalpi_group_layout', self::OPT_LAYOUT, [
            'sanitize_callback' => [ $this, 'sanitize_layout' ],
            'default'           => self::defaults()['layout'],
        ] );
        add_settings_section( 'jalpi_sec_layout', __( 'Default Layout Values', 'jalpi-page-builder' ),
            fn() => $this->section_desc( __( 'Applied to new sections and containers when first created.', 'jalpi-page-builder' ) ),
            'jalpi_page_layout' );
        add_settings_field( 'jalpi_container_width', __( 'Default Container Width', 'jalpi-page-builder' ), [ $this, 'field_container_width' ], 'jalpi_page_layout', 'jalpi_sec_layout' );
        add_settings_field( 'jalpi_section_padding', __( 'Default Section Padding', 'jalpi-page-builder' ), [ $this, 'field_section_padding' ], 'jalpi_page_layout', 'jalpi_sec_layout' );
        add_settings_field( 'jalpi_column_gap',      __( 'Default Column Gap',     'jalpi-page-builder' ), [ $this, 'field_column_gap'       ], 'jalpi_page_layout', 'jalpi_sec_layout' );
    }

    private function register_design(): void {
        register_setting( 'jalpi_group_design', self::OPT_DESIGN, [
            'sanitize_callback' => [ $this, 'sanitize_design' ],
            'default'           => self::defaults()['design'],
        ] );
        add_settings_section( 'jalpi_sec_design', __( 'Global Design Tokens', 'jalpi-page-builder' ),
            fn() => $this->section_desc( __( 'These values are output as CSS custom properties on every Jalpi Page Builder page.', 'jalpi-page-builder' ) ),
            'jalpi_page_design' );
        add_settings_field( 'jalpi_primary_color',   __( 'Primary Color',   'jalpi-page-builder' ), [ $this, 'field_primary_color'   ], 'jalpi_page_design', 'jalpi_sec_design' );
        add_settings_field( 'jalpi_secondary_color', __( 'Secondary Color', 'jalpi-page-builder' ), [ $this, 'field_secondary_color' ], 'jalpi_page_design', 'jalpi_sec_design' );
        add_settings_field( 'jalpi_body_font',       __( 'Body Font',       'jalpi-page-builder' ), [ $this, 'field_body_font'       ], 'jalpi_page_design', 'jalpi_sec_design' );
        add_settings_field( 'jalpi_heading_font',    __( 'Heading Font',    'jalpi-page-builder' ), [ $this, 'field_heading_font'    ], 'jalpi_page_design', 'jalpi_sec_design' );
        add_settings_field( 'jalpi_base_font_size',  __( 'Base Font Size',  'jalpi-page-builder' ), [ $this, 'field_base_font_size'  ], 'jalpi_page_design', 'jalpi_sec_design' );
    }

    private function register_performance(): void {
        register_setting( 'jalpi_group_performance', self::OPT_PERFORMANCE, [
            'sanitize_callback' => [ $this, 'sanitize_performance' ],
            'default'           => self::defaults()['performance'],
        ] );
        add_settings_section( 'jalpi_sec_performance', __( 'Performance Optimisations', 'jalpi-page-builder' ),
            fn() => $this->section_desc( __( 'Fine-tune how Jalpi Page Builder handles assets and caching.', 'jalpi-page-builder' ) ),
            'jalpi_page_performance' );
        add_settings_field( 'jalpi_enable_css_gen', __( 'CSS Generation', 'jalpi-page-builder' ), [ $this, 'field_enable_css_gen' ], 'jalpi_page_performance', 'jalpi_sec_performance' );
        add_settings_field( 'jalpi_enable_lazy',    __( 'Lazy Loading',   'jalpi-page-builder' ), [ $this, 'field_enable_lazy'    ], 'jalpi_page_performance', 'jalpi_sec_performance' );
        add_settings_field( 'jalpi_enable_cache',   __( 'Page Cache',     'jalpi-page-builder' ), [ $this, 'field_enable_cache'   ], 'jalpi_page_performance', 'jalpi_sec_performance' );
    }

    /**
     * Register the site-wide Design System option. This tab has no wp-admin
     * form UI (add_settings_section/add_settings_field) — it is read/written
     * exclusively by the React editor via the /design-system REST route
     * (Jalpi_API::get_design_system/save_design_system). register_setting()
     * is still used so update_option() always runs through the same
     * sanitize_option_{option} safety net as every other Jalpi option.
     */
    private function register_design_system(): void {
        register_setting( 'jalpi_group_design_system', self::OPT_DESIGN_SYSTEM, [
            'sanitize_callback' => [ $this, 'sanitize_design_system_input' ],
            'default'           => self::defaults()['design_system'],
        ] );
    }

    public function sanitize_design_system_input( $input ): array {
        return Jalpi_Sanitizer::sanitize_design_system( is_array( $input ) ? $input : [] );
    }

    /**
     * Fetch the full site-wide Design System token set, merged over defaults.
     *
     * @return array
     */
    public static function get_design_system(): array {
        $defaults = self::defaults()['design_system'];
        $saved    = get_option( self::OPT_DESIGN_SYSTEM, $defaults );
        return is_array( $saved ) ? wp_parse_args( $saved, $defaults ) : $defaults;
    }

    /**
     * Sanitize and persist a full Design System token set.
     *
     * @param array $data Raw (untrusted) token data.
     * @return array The result of get_design_system() after saving — i.e. the
     *               sanitized data merged with defaults for any category the
     *               caller omitted, matching exactly what a subsequent
     *               get_design_system() call will return (callers that need
     *               to verify a save by reading the option back should compare
     *               against this return value, not the raw sanitized input).
     */
    public static function update_design_system( array $data ): array {
        $clean = Jalpi_Sanitizer::sanitize_design_system( $data );
        // autoload=true: this option is read on every frontend page load
        // (output_design_system_css) and every editor boot (jalpiEditorConfig),
        // so it must stay in the cheap `alloptions` cache rather than risk
        // WP's size-heuristic silently demoting it to a per-request query.
        update_option( self::OPT_DESIGN_SYSTEM, $clean, true );
        return self::get_design_system();
    }

    private function register_breakpoints(): void {
        register_setting( 'jalpi_group_breakpoints', self::OPT_BREAKPOINTS, [
            'sanitize_callback' => [ $this, 'sanitize_breakpoints' ],
            'default'           => self::defaults()['breakpoints'],
        ] );
        add_settings_section( 'jalpi_sec_breakpoints', __( 'Responsive Breakpoints', 'jalpi-page-builder' ),
            fn() => $this->section_desc( __( 'Pixel values used to generate @media queries for tablet and mobile responsive overrides.', 'jalpi-page-builder' ) ),
            'jalpi_page_breakpoints' );
        add_settings_field( 'jalpi_tablet_breakpoint', __( 'Tablet Breakpoint', 'jalpi-page-builder' ), [ $this, 'field_tablet_breakpoint' ], 'jalpi_page_breakpoints', 'jalpi_sec_breakpoints' );
        add_settings_field( 'jalpi_mobile_breakpoint', __( 'Mobile Breakpoint', 'jalpi-page-builder' ), [ $this, 'field_mobile_breakpoint' ], 'jalpi_page_breakpoints', 'jalpi_sec_breakpoints' );
    }

    // =========================================================================
    // Page render
    // =========================================================================

    public function render_page(): void {
        if ( ! current_user_can( 'manage_options' ) ) {
            echo '<div class="wrap" style="max-width:520px;margin:60px auto;text-align:center;">'
                . '<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#6366f1" stroke-width="1.5" style="margin-bottom:16px"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>'
                . '<h1 style="font-size:22px;margin:0 0 10px">' . esc_html__( 'Administrator access required', 'jalpi-page-builder' ) . '</h1>'
                . '<p style="color:#646970">' . esc_html__( 'Jalpi Page Builder Settings can only be changed by a WordPress Administrator. Please log in with an Administrator account or ask your site admin for access.', 'jalpi-page-builder' ) . '</p>'
                . '</div>';
            return;
        }

        $this->current_tab = $this->resolve_tab();
        $tabs              = $this->get_tabs();

        ?>
        <div class="wrap jalpi-settings-wrap">

            <div class="jalpi-settings-header">
                <div class="jalpi-settings-header__inner">
                    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0"><rect x="2" y="3" width="20" height="18" rx="2"/><line x1="2" y1="9" x2="22" y2="9"/></svg>
                    <h1 class="jalpi-settings-header__title"><?php esc_html_e( 'Jalpi Page Builder', 'jalpi-page-builder' ); ?> &mdash; <?php esc_html_e( 'Settings', 'jalpi-page-builder' ); ?></h1>
                    <span class="jalpi-settings-header__version">v<?php echo esc_html( JALPI_VERSION ); ?></span>
                </div>
            </div>

            <div class="jalpi-settings-body">

                <nav class="jalpi-settings-tabs" role="tablist" aria-label="<?php esc_attr_e( 'Settings sections', 'jalpi-page-builder' ); ?>">
                    <?php foreach ( $tabs as $slug => $tab ) : ?>
                        <a
                            href="<?php echo esc_url( $this->tab_url( $slug ) ); ?>"
                            class="jalpi-settings-tabs__item<?php echo esc_attr( $this->current_tab === $slug ? ' is-active' : '' ); ?>"
                            role="tab"
                            aria-selected="<?php echo esc_attr( $this->current_tab === $slug ? 'true' : 'false' ); ?>"
                        >
                            <?php if ( ! empty( $tab['icon'] ) ) : ?>
                                <span class="jalpi-settings-tabs__icon" aria-hidden="true"><?php
                                    echo wp_kses( $tab['icon'], [
                                        'svg'      => [ 'width' => true, 'height' => true, 'viewBox' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'style' => true, 'xmlns' => true ],
                                        'circle'   => [ 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true ],
                                        'path'     => [ 'd' => true, 'fill' => true, 'stroke' => true ],
                                        'rect'     => [ 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true ],
                                        'line'     => [ 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'stroke' => true ],
                                        'polyline' => [ 'points' => true ],
                                    ] );
                                ?></span>
                            <?php endif; ?>
                            <?php echo esc_html( $tab['label'] ); ?>
                        </a>
                    <?php endforeach; ?>
                </nav>

                <div class="jalpi-settings-content">
                    <?php settings_errors( 'jalpi_settings' ); ?>

                    <form method="post" action="options.php" novalidate="novalidate">
                        <?php
                        settings_fields( 'jalpi_group_' . $this->current_tab );
                        do_settings_sections( 'jalpi_page_' . $this->current_tab );
                        submit_button( __( 'Save Settings', 'jalpi-page-builder' ), 'primary jalpi-settings-submit' );
                        ?>
                    </form>
                </div>

            </div>
        </div>
        <?php
    }

    // =========================================================================
    // Field renderers — General
    // =========================================================================

    public function field_enable_builder(): void {
        $val = self::get( 'general', 'enable_builder', '1' );
        ?>
        <label class="jalpi-toggle-row">
            <input type="checkbox" name="<?php echo esc_attr( self::OPT_GENERAL . '[enable_builder]' ); ?>" value="1"<?php checked( '1', $val ); ?>>
            <span><?php esc_html_e( 'Enable Jalpi Page Builder across the site', 'jalpi-page-builder' ); ?></span>
        </label>
        <p class="description"><?php esc_html_e( 'When disabled, the editor will not be accessible. Built pages will still render from saved data.', 'jalpi-page-builder' ); ?></p>
        <?php
    }

    public function field_enabled_post_types(): void {
        $saved = (array) self::get( 'general', 'enabled_post_types', [ 'page', 'post' ] );
        $types = $this->get_public_post_types();
        echo '<fieldset><legend class="screen-reader-text">' . esc_html__( 'Supported Post Types', 'jalpi-page-builder' ) . '</legend>';
        foreach ( $types as $type_obj ) {
            $name    = $type_obj->name;
            $label   = $type_obj->labels->singular_name;
            $checked = in_array( $name, $saved, true );
            printf(
                '<label class="jalpi-checkbox-row"><input type="checkbox" name="%s" value="%s"%s><span><strong>%s</strong><code>%s</code></span></label>',
                esc_attr( self::OPT_GENERAL . '[enabled_post_types][]' ),
                esc_attr( $name ),
                checked( $checked, true, false ),
                esc_html( $label ),
                esc_html( $name )
            );
        }
        echo '</fieldset>';
        echo '<p class="description">' . esc_html__( 'Jalpi Page Builder will be available on these post types.', 'jalpi-page-builder' ) . '</p>';
    }

    public function field_disable_gutenberg(): void {
        $saved = (array) self::get( 'general', 'disable_gutenberg', [] );
        $types = $this->get_public_post_types();
        echo '<fieldset><legend class="screen-reader-text">' . esc_html__( 'Disable Gutenberg', 'jalpi-page-builder' ) . '</legend>';
        foreach ( $types as $type_obj ) {
            $name    = $type_obj->name;
            $checked = in_array( $name, $saved, true );
            printf(
                '<label class="jalpi-checkbox-row"><input type="checkbox" name="%s" value="%s"%s><span><strong>%s</strong></span></label>',
                esc_attr( self::OPT_GENERAL . '[disable_gutenberg][]' ),
                esc_attr( $name ),
                checked( $checked, true, false ),
                esc_html( $type_obj->labels->singular_name )
            );
        }
        echo '</fieldset>';
        echo '<p class="description">' . esc_html__( 'Gutenberg will be replaced by the Classic editor on selected post types, making Jalpi Page Builder the primary editor.', 'jalpi-page-builder' ) . '</p>';
    }

    // =========================================================================
    // Field renderers — Layout
    // =========================================================================

    public function field_container_width(): void {
        $val = self::get( 'layout', 'container_width', '1200px' );
        echo '<div class="jalpi-field-inline">';
        printf( '<input type="text" name="%s" value="%s" class="regular-text" placeholder="1200px">', esc_attr( self::OPT_LAYOUT . '[container_width]' ), esc_attr( $val ) );
        echo '</div>';
        echo '<p class="description">' . esc_html__( 'Max-width in Boxed mode. Accepts any valid CSS length, e.g. 1200px, 90%, 80rem.', 'jalpi-page-builder' ) . '</p>';
    }

    public function field_section_padding(): void {
        $val = self::get( 'layout', 'section_padding', '80px 0' );
        printf( '<input type="text" name="%s" value="%s" class="regular-text" placeholder="80px 0">', esc_attr( self::OPT_LAYOUT . '[section_padding]' ), esc_attr( $val ) );
        echo '<p class="description">' . esc_html__( 'CSS padding shorthand for new sections, e.g. "80px 0" or "80px 20px 60px 20px".', 'jalpi-page-builder' ) . '</p>';
    }

    public function field_column_gap(): void {
        $val = self::get( 'layout', 'column_gap', '20px' );
        echo '<div class="jalpi-field-inline">';
        printf( '<input type="text" name="%s" value="%s" class="small-text" placeholder="20px">', esc_attr( self::OPT_LAYOUT . '[column_gap]' ), esc_attr( $val ) );
        echo '</div>';
        echo '<p class="description">' . esc_html__( 'Default gap between columns inside a container.', 'jalpi-page-builder' ) . '</p>';
    }

    // =========================================================================
    // Field renderers — Global Design
    // =========================================================================

    public function field_primary_color(): void {
        $val = self::get( 'design', 'primary_color', '#6366f1' );
        printf( '<input type="text" name="%s" value="%s" class="jalpi-color-picker" data-default-color="#6366f1">', esc_attr( self::OPT_DESIGN . '[primary_color]' ), esc_attr( $val ) );
        echo '<p class="description">CSS variable: <code>--jalpi-primary</code></p>';
    }

    public function field_secondary_color(): void {
        $val = self::get( 'design', 'secondary_color', '#8b5cf6' );
        printf( '<input type="text" name="%s" value="%s" class="jalpi-color-picker" data-default-color="#8b5cf6">', esc_attr( self::OPT_DESIGN . '[secondary_color]' ), esc_attr( $val ) );
        echo '<p class="description">CSS variable: <code>--jalpi-secondary</code></p>';
    }

    public function field_body_font(): void {
        $val = self::get( 'design', 'body_font', '' );
        $this->render_font_select( self::OPT_DESIGN . '[body_font]', $val );
        echo '<p class="description">CSS variable: <code>--jalpi-body-font</code></p>';
    }

    public function field_heading_font(): void {
        $val = self::get( 'design', 'heading_font', '' );
        $this->render_font_select( self::OPT_DESIGN . '[heading_font]', $val );
        echo '<p class="description">CSS variable: <code>--jalpi-heading-font</code></p>';
    }

    /**
     * Render a <select> with fonts grouped by category using <optgroup>.
     */
    private function render_font_select( string $name, string $selected_val ): void {
        $fonts  = self::get_font_options();
        $labels = Jalpi_Google_Fonts::category_labels();

        // Separate special entries (no category) from categorized fonts.
        $special = [];
        $grouped = [];
        foreach ( $fonts as $f ) {
            $cat = $f['category'] ?? '';
            if ( '' === $cat || 'system' === $cat ) {
                $special[] = $f;
            } else {
                $grouped[ $cat ][] = $f;
            }
        }

        printf( '<select name="%s" style="min-width:260px">', esc_attr( $name ) );

        // Special entries first (Site Default, System Default).
        foreach ( $special as $f ) {
            printf(
                '<option value="%s"%s>%s</option>',
                esc_attr( $f['value'] ),
                selected( $selected_val, $f['value'], false ),
                esc_html( $f['label'] )
            );
        }

        // Grouped fonts by category.
        foreach ( $grouped as $cat => $cat_fonts ) {
            $group_label = $labels[ $cat ] ?? ucfirst( $cat );
            printf( '<optgroup label="%s">', esc_attr( $group_label ) );
            foreach ( $cat_fonts as $f ) {
                printf(
                    '<option value="%s"%s>%s</option>',
                    esc_attr( $f['value'] ),
                    selected( $selected_val, $f['value'], false ),
                    esc_html( $f['label'] )
                );
            }
            echo '</optgroup>';
        }

        echo '</select>';
    }

    public function field_base_font_size(): void {
        $val = self::get( 'design', 'base_font_size', '16' );
        echo '<div class="jalpi-field-inline">';
        printf( '<input type="number" name="%s" value="%s" class="small-text" min="10" max="24" step="1">', esc_attr( self::OPT_DESIGN . '[base_font_size]' ), esc_attr( $val ) );
        echo '<span class="jalpi-field-unit">px</span></div>';
        echo '<p class="description">CSS variable: <code>--jalpi-font-size</code>. Base rem scale on Jalpi Page Builder pages.</p>';
    }

    // =========================================================================
    // Field renderers — Performance
    // =========================================================================

    public function field_enable_css_gen(): void {
        $val = self::get( 'performance', 'enable_css_generation', '1' );
        ?>
        <label class="jalpi-toggle-row">
            <input type="checkbox" name="<?php echo esc_attr( self::OPT_PERFORMANCE . '[enable_css_generation]' ); ?>" value="1"<?php checked( '1', $val ); ?>>
            <span><?php esc_html_e( 'Embed generated CSS inline with page output', 'jalpi-page-builder' ); ?></span>
        </label>
        <p class="description"><?php esc_html_e( 'Page-specific styles are embedded in a &lt;style&gt; tag. Disable only if you have a custom CSS pipeline.', 'jalpi-page-builder' ); ?></p>
        <?php
    }

    public function field_enable_lazy(): void {
        $val = self::get( 'performance', 'enable_lazy_loading', '1' );
        ?>
        <label class="jalpi-toggle-row">
            <input type="checkbox" name="<?php echo esc_attr( self::OPT_PERFORMANCE . '[enable_lazy_loading]' ); ?>" value="1"<?php checked( '1', $val ); ?>>
            <span><?php esc_html_e( 'Add loading="lazy" to all Jalpi Page Builder images', 'jalpi-page-builder' ); ?></span>
        </label>
        <p class="description"><?php esc_html_e( 'Uses native browser lazy-loading. Has no effect on browsers that do not support the attribute.', 'jalpi-page-builder' ); ?></p>
        <?php
    }

    public function field_enable_cache(): void {
        $val = self::get( 'performance', 'enable_page_cache', '' );
        ?>
        <label class="jalpi-toggle-row">
            <input type="checkbox" name="<?php echo esc_attr( self::OPT_PERFORMANCE . '[enable_page_cache]' ); ?>" value="1"<?php checked( '1', $val ); ?>>
            <span><?php esc_html_e( 'Purge page cache on save', 'jalpi-page-builder' ); ?></span>
        </label>
        <p class="description"><?php esc_html_e( 'Fires cache purge hooks compatible with W3TC, WP Super Cache, LiteSpeed Cache, and WP Rocket when a Jalpi Page Builder page is saved.', 'jalpi-page-builder' ); ?></p>
        <?php
    }

    // =========================================================================
    // Field renderers — Breakpoints
    // =========================================================================

    public function field_tablet_breakpoint(): void {
        $val = self::get( 'breakpoints', 'tablet_breakpoint', '1024' );
        echo '<div class="jalpi-field-inline">';
        printf( '<input type="number" name="%s" value="%s" class="small-text" min="320" max="2560" step="1">', esc_attr( self::OPT_BREAKPOINTS . '[tablet_breakpoint]' ), esc_attr( $val ) );
        echo '<span class="jalpi-field-unit">px</span></div>';
        echo '<p class="description">' . esc_html__( 'Styles set for "Tablet" apply at max-width of this value (default: 1024px). This is the upper bound — tablet styles apply from this width down.', 'jalpi-page-builder' ) . '</p>';
    }

    public function field_mobile_breakpoint(): void {
        $val = self::get( 'breakpoints', 'mobile_breakpoint', '767' );
        echo '<div class="jalpi-field-inline">';
        printf( '<input type="number" name="%s" value="%s" class="small-text" min="240" max="1024" step="1">', esc_attr( self::OPT_BREAKPOINTS . '[mobile_breakpoint]' ), esc_attr( $val ) );
        echo '<span class="jalpi-field-unit">px</span></div>';
        echo '<p class="description">' . esc_html__( 'Styles set for "Mobile" apply at max-width of this value (default: 767px). Column stacking also uses this breakpoint.', 'jalpi-page-builder' ) . '</p>';
    }

    // =========================================================================
    // Sanitization callbacks
    // =========================================================================

    public function sanitize_general( $input ): array {
        $clean      = self::defaults()['general'];
        $input      = is_array( $input ) ? $input : [];
        $type_names = array_keys( $this->get_public_post_types() );

        $clean['enable_builder'] = ! empty( $input['enable_builder'] ) ? '1' : '';

        $submitted = array_map( 'sanitize_key', (array) ( $input['enabled_post_types'] ?? [] ) );
        $clean['enabled_post_types'] = array_values( array_filter( $submitted, fn( $t ) => in_array( $t, $type_names, true ) ) );

        if ( empty( $clean['enabled_post_types'] ) ) {
            add_settings_error( 'jalpi_settings', 'no_post_type', __( 'At least one post type must be enabled. Defaulting to Pages.', 'jalpi-page-builder' ), 'warning' );
            $clean['enabled_post_types'] = [ 'page' ];
        }

        $submitted_gut = array_map( 'sanitize_key', (array) ( $input['disable_gutenberg'] ?? [] ) );
        $clean['disable_gutenberg'] = array_values( array_filter( $submitted_gut, fn( $t ) => in_array( $t, $type_names, true ) ) );

        // Keep the legacy option in sync so existing code paths still work.
        update_option( 'jalpi_enabled_post_types', $clean['enabled_post_types'] );

        return apply_filters( 'jalpi_sanitize_general_settings', $clean, $input );
    }

    public function sanitize_layout( $input ): array {
        $clean = self::defaults()['layout'];
        $input = is_array( $input ) ? $input : [];

        $clean['container_width'] = Jalpi_Sanitizer::sanitize_dimension( sanitize_text_field( $input['container_width'] ?? '1200px' ) ) ?: '1200px';
        $clean['section_padding'] = $this->sanitize_css_shorthand( sanitize_text_field( $input['section_padding'] ?? '80px 0' ), 4, '80px 0' );
        $clean['column_gap']      = Jalpi_Sanitizer::sanitize_dimension( sanitize_text_field( $input['column_gap']      ?? '20px'  ) ) ?: '20px';

        return apply_filters( 'jalpi_sanitize_layout_settings', $clean, $input );
    }

    public function sanitize_design( $input ): array {
        $clean       = self::defaults()['design'];
        $input       = is_array( $input ) ? $input : [];
        $font_values = array_column( $this->get_font_options(), 'value' );

        $clean['primary_color']   = Jalpi_Sanitizer::sanitize_color( $input['primary_color']   ?? '' ) ?: '#6366f1';
        $clean['secondary_color'] = Jalpi_Sanitizer::sanitize_color( $input['secondary_color'] ?? '' ) ?: '#8b5cf6';

        $body_font = $input['body_font'] ?? '';
        $clean['body_font'] = in_array( $body_font, $font_values, true ) ? $body_font : '';

        $heading_font = $input['heading_font'] ?? '';
        $clean['heading_font'] = in_array( $heading_font, $font_values, true ) ? $heading_font : '';

        $font_size = intval( $input['base_font_size'] ?? 16 );
        $clean['base_font_size'] = (string) min( 24, max( 10, $font_size ) );

        return apply_filters( 'jalpi_sanitize_design_settings', $clean, $input );
    }

    public function sanitize_performance( $input ): array {
        $clean = self::defaults()['performance'];
        $input = is_array( $input ) ? $input : [];

        $clean['enable_css_generation'] = ! empty( $input['enable_css_generation'] ) ? '1' : '';
        $clean['enable_lazy_loading']   = ! empty( $input['enable_lazy_loading']   ) ? '1' : '';
        $clean['enable_page_cache']     = ! empty( $input['enable_page_cache']     ) ? '1' : '';

        return apply_filters( 'jalpi_sanitize_performance_settings', $clean, $input );
    }

    public function sanitize_breakpoints( $input ): array {
        $clean = self::defaults()['breakpoints'];
        $input = is_array( $input ) ? $input : [];

        $tablet = absint( $input['tablet_breakpoint'] ?? 1024 );
        $mobile = absint( $input['mobile_breakpoint'] ?? 767 );

        $tablet = min( 2560, max( 320,  $tablet ) );
        $mobile = min( 1024, max( 240,  $mobile ) );

        // Mobile breakpoint must not exceed tablet breakpoint.
        if ( $mobile > $tablet ) {
            add_settings_error( 'jalpi_settings', 'jalpi_bp_order',
                __( 'Mobile breakpoint cannot be larger than tablet breakpoint. It has been adjusted.', 'jalpi-page-builder' ),
                'warning'
            );
            $mobile = $tablet;
        }

        $clean['tablet_breakpoint'] = (string) $tablet;
        $clean['mobile_breakpoint'] = (string) $mobile;

        return apply_filters( 'jalpi_sanitize_breakpoints_settings', $clean, $input );
    }

    // =========================================================================
    // Frontend hooks
    // =========================================================================

    /**
     * Attach global CSS custom properties to the frontend stylesheet via
     * wp_add_inline_style, and enqueue Google Fonts for global body/heading fonts.
     * Runs on the wp_enqueue_scripts action.
     */
    public function output_global_styles(): void {
        if ( ! is_singular() ) {
            return;
        }

        if ( ! $this->is_jalpi_page( get_the_ID() ) ) {
            return;
        }

        // Ensure the frontend stylesheet is registered so we can attach inline CSS.
        // (Jalpi_Renderer::enqueue_frontend_assets registers it on the same hook.)
        if ( ! wp_style_is( 'jalpi-frontend', 'registered' ) && ! wp_style_is( 'jalpi-frontend', 'enqueued' ) ) {
            return;
        }

        $design = self::get_section( 'design' );

        $primary   = Jalpi_Sanitizer::sanitize_color( $design['primary_color']   ?? '' ) ?: '#6366f1';
        $secondary = Jalpi_Sanitizer::sanitize_color( $design['secondary_color'] ?? '' ) ?: '#8b5cf6';
        $font_size = min( 24, max( 10, intval( $design['base_font_size'] ?? 16 ) ) );
        $body_font = Jalpi_Sanitizer::sanitize_css_value( $design['body_font']    ?? '' );
        $head_font = Jalpi_Sanitizer::sanitize_css_value( $design['heading_font'] ?? '' );

        $vars = apply_filters( 'jalpi_global_css_vars', [
            '--jalpi-primary'      => $primary,
            '--jalpi-secondary'    => $secondary,
            '--jalpi-font-size'    => $font_size . 'px',
            '--jalpi-body-font'    => $body_font ?: 'inherit',
            '--jalpi-heading-font' => $head_font ?: 'inherit',
        ] );

        // Build the CSS custom properties block.
        $css = ":root {\n";
        foreach ( $vars as $prop => $val ) {
            $css .= '  ' . esc_attr( $prop ) . ':' . esc_attr( $val ) . ";\n";
        }
        $css .= '}';

        wp_add_inline_style( 'jalpi-frontend', $css );

        // Load Google Fonts for global body/heading font settings.
        self::enqueue_google_fonts_for( [ $body_font, $head_font ], 'jalpi-global-google-fonts' );
    }

    /**
     * Shared Google-Fonts-URL heuristic, used by both output_global_styles()
     * (legacy body/heading fonts) and output_design_system_css() (fontFamilies
     * tokens + typography/button preset fontFamily fields). Extracted verbatim
     * from the original output_global_styles() font-enqueue block so legacy
     * behavior is unchanged.
     *
     * @param string[] $stacks CSS font-family values, e.g. '"Inter", sans-serif'.
     * @param string   $handle wp_enqueue_style handle to use.
     */
    private static function enqueue_google_fonts_for( array $stacks, string $handle ): void {
        $fonts_needed = array_filter( $stacks );
        if ( ! $fonts_needed ) {
            return;
        }
        $families = [];
        foreach ( $fonts_needed as $stack ) {
            if ( preg_match( '/["\']?([A-Za-z][A-Za-z0-9 ]+)["\']?/', $stack, $m ) ) {
                $families[] = rawurlencode( trim( $m[1] ) );
            }
        }
        $unique = array_unique( $families );
        if ( $unique ) {
            $url = 'https://fonts.googleapis.com/css2?family=' . implode( '&family=', $unique ) . ':wght@400;500;600;700&display=swap';
            wp_enqueue_style( $handle, $url, [], JALPI_VERSION );
        }
    }

    /**
     * Emit the full Design System token set as CSS custom properties on the
     * live frontend — a single, uncached :root block (+ tablet/mobile @media
     * overrides for tokens carrying tabletValue/mobileValue), attached to the
     * same 'jalpi-frontend' stylesheet handle as output_global_styles() and
     * the per-page compiled CSS. Deliberately NOT part of the per-page
     * render transient: it always reads the live option, so editing a token
     * updates every linked widget on every page with zero cache purge.
     */
    public function output_design_system_css(): void {
        if ( ! is_singular() ) {
            return;
        }
        if ( ! $this->is_jalpi_page( get_the_ID() ) ) {
            return;
        }
        if ( ! wp_style_is( 'jalpi-frontend', 'registered' ) && ! wp_style_is( 'jalpi-frontend', 'enqueued' ) ) {
            return;
        }

        $ds     = self::get_design_system();
        $root   = [];
        $tablet = [];
        $mobile = [];
        $fonts  = [];

        foreach ( self::TOKEN_CATEGORIES as $category => $list_key ) {
            $prefix = self::TOKEN_VAR_PREFIX[ $category ];
            foreach ( (array) ( $ds[ $list_key ] ?? [] ) as $t ) {
                $id  = $t['id'] ?? '';
                $val = (string) ( $t['value'] ?? '' );
                if ( '' === $id || '' === $val ) {
                    continue;
                }
                $var         = "--jalpi-{$prefix}-{$id}";
                $root[ $var ] = $val;
                if ( ! empty( $t['tabletValue'] ) ) {
                    $tablet[ $var ] = $t['tabletValue'];
                }
                if ( ! empty( $t['mobileValue'] ) ) {
                    $mobile[ $var ] = $t['mobileValue'];
                }
                if ( 'fontFamily' === $category ) {
                    $fonts[] = $val;
                }
            }
        }

        foreach ( [ 'typo' => 'typography', 'btn' => 'buttons' ] as $kind => $list_key ) {
            foreach ( (array) ( $ds[ $list_key ] ?? [] ) as $preset ) {
                $pid = $preset['id'] ?? '';
                if ( '' === $pid ) {
                    continue;
                }
                foreach ( $preset as $field => $val ) {
                    if ( in_array( $field, [ 'id', 'name' ], true ) || '' === (string) $val ) {
                        continue;
                    }
                    $root[ "--jalpi-{$kind}-{$pid}-" . self::camel_to_kebab( $field ) ] = (string) $val;
                    if ( 'fontFamily' === $field ) {
                        $fonts[] = (string) $val;
                    }
                }
            }
        }

        $vars = apply_filters( 'jalpi_design_system_css_vars', $root );
        $css  = self::vars_to_css_block( ':root', $vars );

        $tablet_bp = (int) self::get( 'breakpoints', 'tablet_breakpoint', 1024 );
        $mobile_bp = (int) self::get( 'breakpoints', 'mobile_breakpoint', 767 );
        if ( $tablet ) {
            $css .= "@media(max-width:{$tablet_bp}px){" . self::vars_to_css_block( ':root', $tablet ) . '}';
        }
        if ( $mobile ) {
            $css .= "@media(max-width:{$mobile_bp}px){" . self::vars_to_css_block( ':root', $mobile ) . '}';
        }

        if ( '' !== $css ) {
            wp_add_inline_style( 'jalpi-frontend', $css );
        }

        self::enqueue_google_fonts_for( $fonts, 'jalpi-design-system-google-fonts' );
    }

    /**
     * Build a `selector{--var:val;...}` string. Values are esc_attr()'d —
     * they have already passed Jalpi_Sanitizer::sanitize_design_system(), this
     * is defense-in-depth against the theoretical case of a malformed/partially
     * bypassed option value reaching output.
     */
    private static function vars_to_css_block( string $selector, array $vars ): string {
        if ( ! $vars ) {
            return '';
        }
        $css = esc_attr( $selector ) . '{';
        foreach ( $vars as $prop => $val ) {
            $css .= esc_attr( $prop ) . ':' . esc_attr( $val ) . ';';
        }
        return $css . '}';
    }

    /** camelCase → kebab-case, used to build --jalpi-typo-{id}-{prop} var names. */
    private static function camel_to_kebab( string $s ): string {
        return strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $s ) );
    }

    /**
     * Disable Gutenberg for post types configured in General settings.
     */
    public function maybe_disable_gutenberg( bool $use_block_editor, string $post_type ): bool {
        if ( ! $use_block_editor ) {
            return false;
        }
        $disabled = (array) self::get( 'general', 'disable_gutenberg', [] );
        return ! in_array( $post_type, $disabled, true );
    }

    // =========================================================================
    // Asset enqueueing
    // =========================================================================

    public function enqueue_assets( string $hook ): void {
        if ( $hook !== $this->page_hook ) {
            return;
        }

        // Iris (WordPress built-in color picker).
        wp_enqueue_style( 'wp-color-picker' );
        wp_enqueue_script( 'wp-color-picker' );

        // Inline admin CSS + color-picker init JS.
        wp_add_inline_style( 'wp-color-picker', $this->admin_css() );
        wp_add_inline_script( 'wp-color-picker', $this->color_picker_js() );
    }

    // =========================================================================
    // Private helpers
    // =========================================================================

    private function resolve_tab(): string {
        $tabs = $this->get_tabs();
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
        $raw = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general';
        return array_key_exists( $raw, $tabs ) ? $raw : 'general';
    }

    private function tab_url( string $tab ): string {
        return add_query_arg( [ 'page' => 'jalpi-page-builder-settings', 'tab' => $tab ], admin_url( 'admin.php' ) );
    }

    private function section_desc( string $text ): void {
        echo '<p class="jalpi-section-desc">' . wp_kses_post( $text ) . '</p>';
    }

    /**
     * Tab definitions — extensible via jalpi_settings_tabs filter.
     * Icons are hardcoded SVG strings (no user input).
     *
     * @return array[]
     */
    private function get_tabs(): array {
        return apply_filters( 'jalpi_settings_tabs', [
            'general'     => [
                'label' => __( 'General',       'jalpi-page-builder' ),
                'icon'  => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M4.93 4.93a10 10 0 0 0 0 14.14"/><path d="M17.66 7.34a6 6 0 0 1 0 8.49"/><path d="M6.34 7.34a6 6 0 0 0 0 8.49"/></svg>',
            ],
            'layout'      => [
                'label' => __( 'Layout',        'jalpi-page-builder' ),
                'icon'  => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="18" rx="2"/><line x1="2" y1="9" x2="22" y2="9"/><line x1="12" y1="9" x2="12" y2="21"/></svg>',
            ],
            'design'      => [
                'label' => __( 'Global Design', 'jalpi-page-builder' ),
                'icon'  => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>',
            ],
            'performance' => [
                'label' => __( 'Performance',   'jalpi-page-builder' ),
                'icon'  => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>',
            ],

            'breakpoints' => [
                'label' => __( 'Breakpoints',   'jalpi-page-builder' ),
                'icon'  => '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/><line x1="8" y1="9" x2="8" y2="12"/><line x1="12" y1="7" x2="12" y2="12"/><line x1="16" y1="10" x2="16" y2="12"/></svg>',
            ],
        ] );
    }

    /**
     * Return public post types as WP_Post_Type objects, keyed by name.
     *
     * @return WP_Post_Type[]
     */
    private function get_public_post_types(): array {
        $raw   = get_post_types( [ 'public' => true ], 'objects' );
        $types = array_filter( $raw, fn( $t ) => ! in_array( $t->name, [ 'attachment' ], true ) );
        return apply_filters( 'jalpi_selectable_post_types', $types );
    }

    /**
     * Return the font select options.
     * Shared by both the Settings page and the Editor (via Jalpi_Editor::get_available_fonts).
     *
     * @return array[]
     */
    public static function get_font_options(): array {
        $base = [
            [ 'label' => '— Site Default —', 'value' => '',         'category' => '' ],
            [ 'label' => 'System Default',   'value' => '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', 'category' => 'system' ],
        ];

        $google = Jalpi_Google_Fonts::instance()->get_font_select_options();

        return apply_filters( 'jalpi_settings_font_options', array_merge( $base, $google ) );
    }

    /**
     * Validate a CSS shorthand string (space-separated dimension tokens).
     * "80px 0" → "80px 0" | "bad value" → $fallback
     */
    private function sanitize_css_shorthand( string $val, int $max_parts = 4, string $fallback = '0' ): string {
        $parts = preg_split( '/\s+/', trim( $val ) );
        $clean = [];
        foreach ( array_slice( $parts, 0, $max_parts ) as $part ) {
            $s = Jalpi_Sanitizer::sanitize_dimension( $part );
            if ( '' !== $s ) {
                $clean[] = $s;
            }
        }
        return $clean ? implode( ' ', $clean ) : $fallback;
    }

    /**
     * Check whether a post was built with Jalpi Page Builder.
     * Uses a lightweight post meta flag instead of querying the jalpi_pages table.
     */
    private function is_jalpi_page( int $post_id ): bool {
        if ( $post_id < 1 ) {
            return false;
        }
        return (bool) get_post_meta( $post_id, '_jalpi_has_canvas_data', true );
    }

    private function admin_css(): string {
        return '
/* ── Wrap ───────────────────────────────────────────────────────── */
.jalpi-settings-wrap { margin:0; }

/* ── Header ─────────────────────────────────────────────────────── */
.jalpi-settings-header {
    background:#fff;
    border-bottom:1px solid #e5e7eb;
    padding:14px 24px;
}
.jalpi-settings-header__inner {
    display:flex; align-items:center; gap:10px;
}
.jalpi-settings-header svg {
    color:#6366f1; flex-shrink:0;
}
.jalpi-settings-header__title {
    margin:0; color:#111827; font-size:17px;
    font-weight:600; line-height:1.3;
}
.jalpi-settings-header__version {
    background:#f3f4f6; color:#6b7280;
    padding:2px 9px; border-radius:20px;
    font-size:11px; font-weight:500;
    letter-spacing:.03em; border:1px solid #e5e7eb;
}

/* ── Body ────────────────────────────────────────────────────────── */
.jalpi-settings-body {
    display:flex; min-height:calc(100vh - 98px);
    background:#f9fafb;
}

/* ── Sidebar tabs ────────────────────────────────────────────────── */
.jalpi-settings-tabs {
    width:176px; flex-shrink:0;
    background:#fff; border-right:1px solid #e5e7eb;
    padding:10px 0;
}
.jalpi-settings-tabs__item {
    display:flex; align-items:center; gap:8px;
    padding:8px 16px; color:#374151;
    text-decoration:none; font-size:13px; font-weight:400;
    border-left:2px solid transparent;
    transition:background .12s, color .12s;
    white-space:nowrap;
}
.jalpi-settings-tabs__item:hover {
    background:#f9fafb; color:#111827;
    border-left-color:#d1d5db;
}
.jalpi-settings-tabs__item.is-active {
    background:#eef2ff; border-left-color:#6366f1;
    color:#4f46e5; font-weight:600;
}
.jalpi-settings-tabs__icon {
    display:flex; flex-shrink:0; opacity:.55;
}
.jalpi-settings-tabs__item:hover .jalpi-settings-tabs__icon { opacity:.8; }
.jalpi-settings-tabs__item.is-active .jalpi-settings-tabs__icon {
    opacity:1; color:#6366f1;
}

/* ── Content pane ────────────────────────────────────────────────── */
.jalpi-settings-content {
    flex:1; padding:20px 24px; background:#f9fafb;
}
.jalpi-settings-content form {
    background:#fff; border:1px solid #e5e7eb;
    border-radius:8px; padding:4px 20px 18px;
}
.jalpi-settings-content .form-table th {
    padding:15px 0; width:195px;
    font-size:13px; color:#111827; font-weight:500;
}
.jalpi-settings-content .form-table td { padding:13px 8px; }
.jalpi-settings-content h2 {
    font-size:13.5px; font-weight:600; color:#111827;
    border-bottom:1px solid #f3f4f6;
    padding:18px 0 10px; margin:0 0 2px;
}
.jalpi-settings-content .notice { margin:12px 0 4px; }

/* ── Section description ─────────────────────────────────────────── */
.jalpi-section-desc {
    color:#6b7280; font-size:12.5px; margin:0 0 6px;
}

/* ── Toggle & checkbox rows ──────────────────────────────────────── */
.jalpi-toggle-row {
    display:flex; align-items:center; gap:9px; cursor:pointer;
}
.jalpi-toggle-row input[type="checkbox"] { margin:0; }
.jalpi-checkbox-row {
    display:flex; align-items:center; gap:9px;
    margin-bottom:7px; cursor:pointer;
}
.jalpi-checkbox-row input { margin:0; }
.jalpi-checkbox-row code {
    background:#f3f4f6; padding:1px 5px; border-radius:3px;
    font-size:11px; color:#6b7280; border:1px solid #e5e7eb;
}

/* ── Inline field ────────────────────────────────────────────────── */
.jalpi-field-inline { display:flex; align-items:center; gap:7px; }
.jalpi-field-unit { color:#9ca3af; font-size:13px; font-weight:500; }

/* ── Locked field ────────────────────────────────────────────────── */
.jalpi-field-locked {
    display:inline-flex; align-items:center; gap:7px; color:#6b7280;
    background:#f9fafb; border-left:3px solid #e5e7eb;
    padding:8px 12px; border-radius:4px; font-size:13px;
}

/* ── Save button ─────────────────────────────────────────────────── */
.jalpi-settings-submit { margin-top:8px !important; }
        ';
    }

    private function color_picker_js(): string {
        return "
(function($){
    $(function(){
        if (typeof $.fn.wpColorPicker === 'undefined') return;
        $('.jalpi-color-picker').wpColorPicker();
    });
}(jQuery));
        ";
    }
}
