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

/**
 * Input sanitization and validation for all Jalpi Page Builder node data.
 *
 * Called once, at save time, so the renderer can trust stored values.
 * Each helper is public so the renderer and other classes can reuse them.
 */
class Jalpi_Sanitizer {

    // -------------------------------------------------------------------------
    // Public entry points
    // -------------------------------------------------------------------------

    /**
     * Sanitize a full page-data array received from the REST API.
     *
     * @param  mixed $data Raw decoded JSON from the request.
     * @return array       Clean, safe page data.
     */
    public static function sanitize_page_data( $data ): array {
        if ( ! is_array( $data ) ) {
            return [ 'version' => '1.0', 'sections' => [] ];
        }

        // Reset recursion guards for this sanitization pass.
        self::$depth    = 0;
        self::$seen_ids = [];

        $clean = [
            'version'  => sanitize_text_field( $data['version'] ?? '1.0' ),
            'sections' => array_values( array_filter(
                array_map( [ self::class, 'sanitize_node' ], (array) ( $data['sections'] ?? [] ) )
            ) ),
        ];

        // Preserve globalStyles if present (colours, typography, button presets, spacing)
        if ( isset( $data['globalStyles'] ) && is_array( $data['globalStyles'] ) ) {
            $clean['globalStyles'] = self::sanitize_global_styles( $data['globalStyles'] );
        }

        return $clean;
    }

    /** Maximum nesting depth to prevent infinite recursion from circular references. */
    private const MAX_DEPTH = 20;

    /** Current recursion depth during sanitize_node(). */
    private static int $depth = 0;

    /** Seen node IDs during current sanitization pass (circular reference guard). */
    private static array $seen_ids = [];

    /**
     * Recursively sanitize a single node and all its children.
     *
     * Guards against circular references (duplicate IDs) and excessive nesting
     * depth to prevent infinite loops and stack overflows.
     *
     * @param  mixed $node Raw node array.
     * @return array       Sanitized node, or empty array for unknown types (filtered out by caller).
     */
    public static function sanitize_node( $node ): array {
        if ( ! is_array( $node ) ) {
            return [];
        }

        // Guard: prevent excessive nesting depth.
        if ( self::$depth >= self::MAX_DEPTH ) {
            return [];
        }

        $allowed_types = [
            // Structural
            'section', 'container', 'column',
            // Basic
            'heading', 'text', 'image', 'button', 'spacer', 'divider',
            'icon', 'shortcode', 'logo',
            // Media
            'video', 'maps',
            // Forms
            'form',
            // Custom HTML
            'html',
            // Activated elements
            'alert', 'counter', 'progress', 'testimonial', 'accordion',
            // New elements
            'badge', 'icon-list', 'pricing-table', 'team-member', 'faq',
            'table', 'feature-card', 'cta', 'stats-card', 'timeline',
            'testimonials',
            // New widgets
            'tabs', 'tabs-adv', 'split-text', 'menu', 'adv-menu', 'breadcrumbs', 'image-box', 'social-icons', 'gallery', 'image-carousel',
            'countdown', 'flip-box', 'star-rating',
            // WooCommerce
            'products-grid', 'product-categories', 'single-product',
        ];

        // Schema-driven widgets (registry-defined, generic render/sanitize
        // path). Appended so a mismatch can never silently delete a valid new
        // widget — the schema file is the single source of truth for these.
        if ( function_exists( 'jalpi_widget_schema_types' ) ) {
            $allowed_types = array_merge( $allowed_types, jalpi_widget_schema_types() );
        }

        $type = sanitize_key( $node['type'] ?? '' );
        if ( ! in_array( $type, $allowed_types, true ) ) {
            return [];
        }

        $clean = [
            'id'   => sanitize_key( $node['id'] ?? '' ),
            'type' => $type,
        ];

        if ( ! $clean['id'] ) {
            return []; // Nodes must have an ID
        }

        // Guard: prevent circular references (same ID appearing twice in the tree).
        if ( isset( self::$seen_ids[ $clean['id'] ] ) ) {
            return [];
        }
        self::$seen_ids[ $clean['id'] ] = true;

        if ( isset( $node['content'] ) ) {
            $clean['content'] = self::sanitize_content( $type, (array) $node['content'] );
        }

        if ( isset( $node['styles'] ) ) {
            $clean['styles'] = self::sanitize_styles( (array) $node['styles'] );
        }

        foreach ( [ 'tabletStyles', 'mobileStyles' ] as $resp_key ) {
            if ( isset( $node[ $resp_key ] ) ) {
                $clean[ $resp_key ] = self::sanitize_styles( (array) $node[ $resp_key ] );
            }
        }

        if ( isset( $node['settings'] ) ) {
            $clean['settings'] = self::sanitize_settings( $type, (array) $node['settings'] );
        }

        if ( ! empty( $node['children'] ) && is_array( $node['children'] ) ) {
            self::$depth++;
            $clean['children'] = array_values( array_filter(
                array_map( [ self::class, 'sanitize_node' ], $node['children'] )
            ) );
            self::$depth--;
        }

        // Responsive visibility flags
        if ( isset( $node['hideOn'] ) && is_array( $node['hideOn'] ) ) {
            $clean['hideOn'] = [
                'desktop' => (bool) ( $node['hideOn']['desktop'] ?? false ),
                'tablet'  => (bool) ( $node['hideOn']['tablet']  ?? false ),
                'mobile'  => (bool) ( $node['hideOn']['mobile']  ?? false ),
            ];
        }

        // Layer lock
        if ( isset( $node['locked'] ) ) {
            $clean['locked'] = (bool) $node['locked'];
        }

        // Global style reference
        if ( isset( $node['globalStyleRef'] ) && is_array( $node['globalStyleRef'] ) ) {
            $clean['globalStyleRef'] = self::sanitize_global_style_ref_data( $node['globalStyleRef'] );
        }

        // Interaction Engine — entrance/scroll/hover/etc. animation rules.
        if ( isset( $node['interactions'] ) && is_array( $node['interactions'] ) ) {
            $clean['interactions'] = self::sanitize_interactions( $node['interactions'] );
        }

        return $clean;
    }

    /**
     * Sanitize a Design System link ({typographyId?, buttonId?, tokens?}).
     * Shared by node.globalStyleRef and class definitions (Class-Based Styling
     * System) so both inherit the same site-wide tokens/presets the same way.
     */
    private static function sanitize_global_style_ref_data( array $raw ): array {
        $ref = [
            'typographyId' => sanitize_key( $raw['typographyId'] ?? '' ),
            'buttonId'     => sanitize_key( $raw['buttonId']     ?? '' ),
        ];

        // Generic per-property Design System token links: styleProp -> {category,id}.
        // Tolerant of malformed/tampered entries — dropped silently, never fatal.
        $tokens = [];
        foreach ( (array) ( $raw['tokens'] ?? [] ) as $prop => $t ) {
            if ( ! is_array( $t ) ) continue;
            $prop_key = sanitize_key( (string) $prop );
            $category = sanitize_key( $t['category'] ?? '' );
            $id       = sanitize_key( $t['id'] ?? '' );
            if ( ! $prop_key || ! $id || ! array_key_exists( $category, Jalpi_Settings::TOKEN_CATEGORIES ) ) continue;
            $tokens[ $prop_key ] = [ 'category' => $category, 'id' => $id ];
        }
        if ( $tokens ) {
            $ref['tokens'] = $tokens;
        }

        return $ref;
    }

    // -------------------------------------------------------------------------
    // Interaction Engine — entrance/scroll/hover/click/etc. animation rules.
    // Consumed by Jalpi_Renderer::apply_interactions()/maybe_add_interaction_css()
    // and admin/editor/src assets/js/jalpi-frontend.js's initInteractions().
    // -------------------------------------------------------------------------

    private const INTERACTION_TRIGGERS = [
        'pageLoad', 'hover', 'parentHover', 'mouseEnter', 'mouseLeave', 'click', 'doubleClick',
        'scrollIntoView', 'scrollProgress', 'mouseMove', 'focus', 'blur',
        // Advanced motion primitives handled entirely in the frontend engine
        // (see registerTilt/Magnetic/Ripple/Pin/HScroll in jalpi-frontend.js).
        // They drive their own transforms, so they need no keyframe CSS.
        'tilt', 'magnetic', 'ripple', 'pin', 'hscroll',
    ];

    private const INTERACTION_ACTIONS = [
        'fade', 'slide', 'scale', 'rotate', 'translate', 'opacity', 'blur',
        'skew', 'flip', 'bounce', 'zoom', 'customTransform', 'jackInTheBox',
        'elastic', 'reveal', 'wipe',
    ];

    private const INTERACTION_CLIP_SHAPES = [ 'edge', 'circle', 'diamond', 'square' ];

    /**
     * Sanitize a node's `interactions` array. Tolerant of malformed/tampered
     * entries — an invalid rule is dropped silently, never fatal, matching the
     * philosophy already used for globalStyleRef.tokens above.
     *
     * @param array $rules Raw (untrusted) rule list.
     * @return array
     */
    private static function sanitize_interactions( array $rules ): array {
        $clean = [];
        foreach ( $rules as $rule ) {
            if ( ! is_array( $rule ) ) continue;

            // Note: trigger/action are intentionally NOT run through sanitize_key()
            // — it lowercases (sanitize_key('scrollIntoView') === 'scrollintoview'),
            // which would never match these camelCase allow-list values. Strict
            // in_array() membership against a fixed, hardcoded allow-list is
            // itself a complete validation, so no separate sanitizer is needed.
            $id      = sanitize_key( $rule['id'] ?? '' );
            $trigger = is_string( $rule['trigger'] ?? null ) ? $rule['trigger'] : '';
            $action  = is_string( $rule['action']  ?? null ) ? $rule['action']  : '';

            if ( ! $id || ! in_array( $trigger, self::INTERACTION_TRIGGERS, true ) || ! in_array( $action, self::INTERACTION_ACTIONS, true ) ) {
                continue;
            }

            $out = [
                'id'      => $id,
                'trigger' => $trigger,
                'action'  => $action,
                'enabled' => ! isset( $rule['enabled'] ) || (bool) $rule['enabled'],
            ];

            $settings = is_array( $rule['settings'] ?? null ) ? self::sanitize_interaction_settings( $rule['settings'] ) : [];
            if ( $settings ) {
                $out['settings'] = $settings;
            }

            // Per-breakpoint enable/disable. Absent/empty = enabled on every
            // breakpoint, identical to every rule saved before this field
            // existed — no migration needed for old content.
            if ( is_array( $rule['responsive'] ?? null ) ) {
                $responsive = [];
                foreach ( [ 'desktop', 'tablet', 'mobile' ] as $tier ) {
                    if ( isset( $rule['responsive'][ $tier ] ) ) {
                        $responsive[ $tier ] = (bool) $rule['responsive'][ $tier ];
                    }
                }
                if ( $responsive ) {
                    $out['responsive'] = $responsive;
                }
            }

            $clean[] = $out;
        }
        return $clean;
    }

    /**
     * Sanitize one rule's `settings` bag. Every key is optional — only
     * recognised keys survive, everything else (including unknown keys from a
     * future/edited client) is dropped rather than erroring.
     */
    private static function sanitize_interaction_settings( array $s ): array {
        $clean = [];

        // Generic "Advanced" controls — dimension/numeric values.
        foreach ( [
            'duration', 'delay', 'distance', 'rotationDeg', 'scaleValue', 'blurAmount',
            'opacityValue', 'startOffset', 'endOffset', 'viewportPct',
            'moveX', 'moveY', 'strength', 'smoothness', 'transitionDuration',
        ] as $key ) {
            if ( ! isset( $s[ $key ] ) || '' === $s[ $key ] ) continue;
            $val = self::sanitize_dimension( (string) $s[ $key ] );
            if ( '' !== $val ) $clean[ $key ] = $val;
        }

        // Repeat: either a positive number (string) or the literal "infinite".
        if ( isset( $s['repeat'] ) && '' !== $s['repeat'] ) {
            $raw = strtolower( trim( (string) $s['repeat'] ) );
            if ( 'infinite' === $raw ) {
                $clean['repeat'] = 'infinite';
            } else {
                $val = self::sanitize_dimension( $raw );
                if ( '' !== $val ) $clean['repeat'] = $val;
            }
        }

        // Booleans.
        foreach ( [ 'infinite', 'onceOnly', 'replay', 'reverse', 'hideAfterPlay' ] as $key ) {
            if ( isset( $s[ $key ] ) ) $clean[ $key ] = (bool) $s[ $key ];
        }

        // Easing: named CSS timing-function keywords, or a cubic-bezier() curve
        // (interaction engines commonly need bounce/elastic-style overshoot
        // curves that the 5 basic keywords can't express).
        if ( ! empty( $s['easing'] ) ) {
            $val = (string) $s['easing'];
            $named = [ 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out' ];
            if ( in_array( $val, $named, true ) ) {
                $clean['easing'] = $val;
            } elseif ( preg_match( '/^cubic-bezier\(\s*-?[\d.]+\s*,\s*-?[\d.]+\s*,\s*-?[\d.]+\s*,\s*-?[\d.]+\s*\)$/', $val ) ) {
                $clean['easing'] = $val;
            }
        }

        // Direction: doubles as slide-direction (up/down/left/right) and CSS
        // animation-direction (normal/reverse/alternate/alternate-reverse) —
        // which one applies depends on the rule's `action`, resolved at render
        // time, not here.
        if ( ! empty( $s['direction'] ) && in_array( $s['direction'], [ 'up', 'down', 'left', 'right', 'normal', 'reverse', 'alternate', 'alternate-reverse' ], true ) ) {
            $clean['direction'] = $s['direction'];
        }

        if ( ! empty( $s['scrollDirection'] ) && in_array( $s['scrollDirection'], [ 'vertical', 'horizontal', 'both' ], true ) ) {
            $clean['scrollDirection'] = $s['scrollDirection'];
        }

        foreach ( [ 'hoverInAction', 'hoverOutAction' ] as $key ) {
            if ( ! empty( $s[ $key ] ) && in_array( $s[ $key ], self::INTERACTION_ACTIONS, true ) ) {
                $clean[ $key ] = $s[ $key ];
            }
        }

        // Compound CSS values.
        if ( ! empty( $s['transformOrigin'] ) ) {
            $val = self::sanitize_css_value( (string) $s['transformOrigin'] );
            if ( '' !== $val ) $clean['transformOrigin'] = $val;
        }
        if ( ! empty( $s['customTransform'] ) ) {
            $val = self::sanitize_css_value( (string) $s['customTransform'] );
            if ( '' !== $val ) $clean['customTransform'] = $val;
        }
        if ( ! empty( $s['customFilter'] ) ) {
            $val = self::sanitize_css_value( (string) $s['customFilter'] );
            if ( '' !== $val ) $clean['customFilter'] = $val;
        }

        if ( ! empty( $s['clipShape'] ) && in_array( $s['clipShape'], self::INTERACTION_CLIP_SHAPES, true ) ) {
            $clean['clipShape'] = $s['clipShape'];
        }

        return $clean;
    }

    // -------------------------------------------------------------------------
    // Content sanitizers — one branch per element type
    // -------------------------------------------------------------------------

    private static function sanitize_content( string $type, array $content ): array {
        switch ( $type ) {

            case 'heading':
                return [
                    'text' => wp_kses_post( $content['text'] ?? '' ),
                    'tag'  => in_array( $content['tag'] ?? 'h2', [ 'h1','h2','h3','h4','h5','h6' ], true )
                              ? $content['tag'] : 'h2',
                ];

            case 'text':
                return [
                    'html' => wp_kses_post( $content['html'] ?? $content['text'] ?? '' ),
                ];

            case 'image':
                return [
                    'url'       => self::sanitize_url( $content['url']     ?? '' ),
                    'alt'       => sanitize_text_field( $content['alt']    ?? '' ),
                    'caption'   => sanitize_text_field( $content['caption'] ?? '' ),
                    'linkUrl'   => self::sanitize_url( $content['linkUrl'] ?? '' ),
                    'newTab'    => (bool) ( $content['newTab'] ?? false ),
                    'imageSize' => sanitize_key( $content['imageSize']     ?? '' ),
                    'width'     => self::sanitize_dimension( $content['width']  ?? '' ),
                    'height'    => self::sanitize_dimension( $content['height'] ?? '' ),
                ];

            case 'button':
                $btn = [
                    'text'   => sanitize_text_field( $content['text'] ?? '' ),
                    'url'    => self::sanitize_url( $content['url'] ?? '#' ),
                    'newTab' => (bool) ( $content['newTab'] ?? false ),
                ];
                if ( isset( $content['icon'] ) ) {
                    $btn['icon']         = self::sanitize_icon_value( $content['icon'], '' );
                    $btn['iconPosition'] = in_array( $content['iconPosition'] ?? 'before', [ 'before','after' ], true ) ? $content['iconPosition'] : 'before';
                    $btn['iconSpacing']  = absint( $content['iconSpacing'] ?? 6 );
                }
                return $btn;

            case 'video': {
                $allowed_src = [ 'youtube', 'vimeo', 'self' ];
                $allowed_sug = [ 'current', 'any' ];
                return [
                    'source'          => in_array( $content['source'] ?? 'youtube', $allowed_src, true ) ? $content['source'] : 'youtube',
                    'url'             => self::sanitize_url( $content['url'] ?? '' ),
                    'startTime'       => absint( $content['startTime'] ?? 0 ) ?: '',
                    'endTime'         => absint( $content['endTime']   ?? 0 ) ?: '',
                    'autoplay'        => (bool) ( $content['autoplay']    ?? false ),
                    'muted'           => (bool) ( $content['muted']       ?? false ),
                    'loop'            => (bool) ( $content['loop']        ?? false ),
                    'controls'        => (bool) ( $content['controls']    ?? true  ),
                    'captions'        => (bool) ( $content['captions']    ?? false ),
                    'privacyMode'     => (bool) ( $content['privacyMode'] ?? false ),
                    'lazyLoad'        => (bool) ( $content['lazyLoad']    ?? true  ),
                    'suggestedVideos' => in_array( $content['suggestedVideos'] ?? 'current', $allowed_sug, true ) ? $content['suggestedVideos'] : 'current',
                ];
            }

            case 'maps':
                return [
                    'address' => sanitize_text_field( $content['address'] ?? '' ),
                    'height'  => self::sanitize_dimension( $content['height'] ?? '400px' ),
                ];

            case 'html':
                // Requires unfiltered_html capability. Contributors/editors receive wp_kses_post().
                return [
                    'html' => current_user_can( 'unfiltered_html' )
                              ? wp_kses( $content['html'] ?? '', self::kses_html_block() )
                              : wp_kses_post( $content['html'] ?? '' ),
                ];

            case 'icon':
                // Only structural/content fields. Visual settings (color, size, padding, rotate)
                // are stored in node.styles via the Style tab and sanitized via sanitize_styles().
                $icon_view = $content['view'] ?? 'default';
                $icon_out = [
                    'icon' => self::sanitize_icon_value( $content['icon'] ?? null, '' ),
                    'view' => in_array( $icon_view, [ 'default','stacked','framed' ], true )
                              ? $icon_view : 'default',
                ];
                if ( isset( $content['borderRadius'] ) )  $icon_out['borderRadius'] = sanitize_text_field( $content['borderRadius'] );
                if ( ! empty( $content['link'] ) )        $icon_out['link']         = self::sanitize_url( $content['link'] );
                if ( isset( $content['newTab'] ) )        $icon_out['newTab']       = (bool) $content['newTab'];
                // Legacy: preserve viewColor if saved by older versions (renderer still reads it as fallback)
                if ( ! empty( $content['viewColor'] ) )   $icon_out['viewColor']    = self::sanitize_color( $content['viewColor'] );
                return $icon_out;

            case 'shortcode':
                // Only users who can publish posts may store shortcodes.
                return [
                    'shortcode' => current_user_can( 'edit_posts' )
                                   ? sanitize_text_field( $content['shortcode'] ?? '' )
                                   : '',
                ];

            case 'logo':
                return [
                    'url'     => self::sanitize_url( $content['url']     ?? '' ),
                    'alt'     => sanitize_text_field( $content['alt']    ?? '' ),
                    'width'   => self::sanitize_dimension( $content['width']  ?? '' ),
                    'height'  => self::sanitize_dimension( $content['height'] ?? '' ),
                    'linkUrl' => self::sanitize_url( $content['linkUrl'] ?? '' ),
                    'newTab'  => (bool) ( $content['newTab'] ?? false ),
                ];

            case 'form':
                return [
                    'plugin'  => sanitize_key( $content['plugin']  ?? '' ),
                    'form_id' => sanitize_text_field( $content['form_id'] ?? '' ),
                ];

            // ── Alert ──────────────────────────────────────────────────────────
            case 'alert':
                return [
                    'variant' => in_array( $content['variant'] ?? 'info', [ 'info','success','warning','error' ], true )
                                 ? $content['variant'] : 'info',
                    'title'   => sanitize_text_field( $content['title'] ?? '' ),
                    'text'    => wp_kses_post( $content['text'] ?? '' ),
                ];

            // ── Counter ────────────────────────────────────────────────────────
            case 'counter': {
                $allowed_sep = [ 'dot', 'comma', 'space' ];

                $sanitize_item = static function( array $it ) use ( $allowed_sep ): array {
                    return [
                        'startNumber' => sanitize_text_field( $it['startNumber'] ?? '0' ),
                        'endNumber'   => sanitize_text_field( $it['endNumber']   ?? $it['number'] ?? '100' ),
                        'prefix'      => sanitize_text_field( $it['prefix']  ?? '' ),
                        'suffix'      => sanitize_text_field( $it['suffix']  ?? '' ),
                        'duration'    => absint( $it['duration']    ?? 2000 ),
                        'thousandSep' => (bool) ( $it['thousandSep'] ?? false ),
                        'separator'   => in_array( $it_sep = ( $it['separator'] ?? 'dot' ), $allowed_sep, true ) ? $it_sep : 'dot',
                        'title'       => sanitize_text_field( $it['title'] ?? $it['label'] ?? '' ),
                    ];
                };

                // New format: items array
                if ( ! empty( $content['items'] ) && is_array( $content['items'] ) ) {
                    $items = [];
                    foreach ( $content['items'] as $it ) {
                        if ( is_array( $it ) ) $items[] = $sanitize_item( $it );
                    }
                    return [ 'items' => $items ];
                }

                // Legacy single-counter format — wrap in items array for uniform output
                return [ 'items' => [ $sanitize_item( $content ) ] ];
            }

            // ── Progress ───────────────────────────────────────────────────────
            case 'progress': {
                $allowed_tags = [ 'h1','h2','h3','h4','h5','h6','div','span' ];
                $items = [];
                foreach ( (array) ( $content['items'] ?? [] ) as $item ) {
                    if ( ! is_array( $item ) ) continue;
                    $items[] = [
                        'title'          => sanitize_text_field( $item['title'] ?? $item['label'] ?? '' ),
                        'titleTag'       => in_array( $it_tag = ( $item['titleTag'] ?? 'h3' ), $allowed_tags, true ) ? $it_tag : 'h3',
                        'showTitle'      => isset( $item['showTitle'] ) ? (bool) $item['showTitle'] : true,
                        'percentage'     => min( 100, max( 0, intval( $item['percentage'] ?? 0 ) ) ),
                        'showPercentage' => isset( $item['showPercentage'] ) ? (bool) $item['showPercentage'] : true,
                        'innerText'      => sanitize_text_field( $item['innerText'] ?? '' ),
                        'color'          => self::sanitize_color( $item['color'] ?? '#1a1a1a' ) ?: '#1a1a1a',
                    ];
                }
                return [ 'items' => $items ];
            }

            // ── Testimonial (single) ───────────────────────────────────────────
            case 'testimonial':
                return [
                    'text'      => wp_kses_post( $content['text']      ?? '' ),
                    'name'      => sanitize_text_field( $content['name']      ?? '' ),
                    'role'      => sanitize_text_field( $content['role']      ?? '' ),
                    'avatarUrl' => self::sanitize_url( $content['avatarUrl']  ?? '' ),
                    'rating'    => min( 5, max( 1, intval( $content['rating'] ?? 5 ) ) ),
                ];

            // ── Accordion ─────────────────────────────────────────────────────
            case 'accordion': {
                $items = [];
                foreach ( (array) ( $content['items'] ?? [] ) as $item ) {
                    if ( ! is_array( $item ) ) continue;
                    $items[] = [
                        'title' => sanitize_text_field( $item['title'] ?? '' ),
                        'body'  => wp_kses_post( $item['body'] ?? '' ),
                    ];
                }
                $allowed_pos  = [ 'left', 'center', 'right', 'justify' ];
                $allowed_icon = [ 'none', 'default', 'plus', 'minus' ];
                return [
                    'items'        => $items,
                    'itemPosition' => in_array( $content['itemPosition'] ?? 'left', $allowed_pos,  true ) ? $content['itemPosition'] : 'left',
                    'iconPosition' => in_array( $content['iconPosition'] ?? 'right', [ 'left', 'right' ], true ) ? $content['iconPosition'] : 'right',
                    'expandIcon'   => in_array( $content['expandIcon']   ?? 'default', $allowed_icon, true ) ? $content['expandIcon']   : 'default',
                    'collapseIcon' => in_array( $content['collapseIcon'] ?? 'default', $allowed_icon, true ) ? $content['collapseIcon'] : 'default',
                ];
            }

            // ── Badge ──────────────────────────────────────────────────────────
            case 'badge':
                return [
                    'text'    => sanitize_text_field( $content['text']    ?? '' ),
                    'variant' => in_array( $content['variant'] ?? 'primary', [ 'primary','secondary','success','warning','error','info' ], true )
                                 ? $content['variant'] : 'primary',
                ];

            // ── Icon List ─────────────────────────────────────────────────────
            case 'icon-list': {
                $allowed_layouts = [ 'list', 'inline' ];
                $allowed_link_on = [ 'full', 'icon', 'text' ];

                $layout_raw    = $content['layout']      ?? 'list';
                $link_on_raw   = $content['applyLinkOn'] ?? 'full';
                $layout       = in_array( $layout_raw,  $allowed_layouts, true ) ? $layout_raw  : 'list';
                $apply_link   = in_array( $link_on_raw, $allowed_link_on, true ) ? $link_on_raw : 'full';
                $default_icon = self::sanitize_icon_value( $content['defaultIcon'] ?? 'check', 'check' );

                $raw_items = (array) ( $content['items'] ?? [] );
                $items     = [];
                foreach ( $raw_items as $item ) {
                    if ( is_string( $item ) ) {
                        $items[] = [ 'text' => sanitize_text_field( $item ), 'icon' => '', 'link' => '', 'newTab' => false ];
                    } elseif ( is_array( $item ) ) {
                        $items[] = [
                            'text'   => sanitize_text_field( $item['text']   ?? '' ),
                            'icon'   => self::sanitize_icon_value( $item['icon'] ?? '', '' ),
                            'link'   => self::sanitize_url( $item['link']    ?? '' ),
                            'newTab' => (bool) ( $item['newTab'] ?? false ),
                        ];
                    }
                }

                return [
                    'layout'      => $layout,
                    'defaultIcon' => $default_icon,
                    'applyLinkOn' => $apply_link,
                    'items'       => array_values( $items ),
                ];
            }

            // ── Pricing Table ─────────────────────────────────────────────────
            case 'pricing-table': {
                $features = array_map( 'sanitize_text_field', (array) ( $content['features'] ?? [] ) );
                return [
                    'name'     => sanitize_text_field( $content['name']     ?? '' ),
                    'tagline'  => sanitize_text_field( $content['tagline']  ?? '' ),
                    'currency' => sanitize_text_field( $content['currency'] ?? '$' ),
                    'amount'   => sanitize_text_field( $content['amount']   ?? '0' ),
                    'period'   => sanitize_text_field( $content['period']   ?? 'mo' ),
                    'features' => array_values( $features ),
                    'btnText'  => sanitize_text_field( $content['btnText']  ?? '' ),
                    'btnUrl'   => self::sanitize_url( $content['btnUrl']    ?? '#' ),
                    'featured' => (bool) ( $content['featured'] ?? false ),
                ];
            }

            // ── Team Member ───────────────────────────────────────────────────
            case 'team-member':
                return [
                    'photoUrl' => self::sanitize_url( $content['photoUrl'] ?? '' ),
                    'name'     => sanitize_text_field( $content['name']    ?? '' ),
                    'role'     => sanitize_text_field( $content['role']    ?? '' ),
                    'bio'      => wp_kses_post( $content['bio']            ?? '' ),
                    'linkedin' => self::sanitize_url( $content['linkedin'] ?? '' ),
                    'twitter'  => self::sanitize_url( $content['twitter']  ?? '' ),
                ];

            // ── FAQ ───────────────────────────────────────────────────────────
            case 'faq': {
                $items = [];
                foreach ( (array) ( $content['items'] ?? [] ) as $item ) {
                    if ( ! is_array( $item ) ) continue;
                    $items[] = [
                        'question'    => sanitize_text_field( $item['question']    ?? '' ),
                        'answer'      => wp_kses_post( $item['answer']             ?? '' ),
                        'defaultOpen' => ! empty( $item['defaultOpen'] ),
                    ];
                }
                return [ 'items' => $items ];
            }

            // ── Table ─────────────────────────────────────────────────────────
            case 'table': {
                $headers = array_map( 'sanitize_text_field', (array) ( $content['headers'] ?? [] ) );
                $rows    = [];
                foreach ( (array) ( $content['rows'] ?? [] ) as $row ) {
                    if ( ! is_array( $row ) ) continue;
                    $rows[] = array_values( array_map( 'sanitize_text_field', $row ) );
                }
                return [
                    'headers' => array_values( $headers ),
                    'rows'    => $rows,
                    'caption' => sanitize_text_field( $content['caption'] ?? '' ),
                ];
            }

            // ── Feature Card ──────────────────────────────────────────────────
            case 'feature-card': {
                $allowed_views = [ 'default', 'stacked', 'framed' ];
                return [
                    'icon'       => self::sanitize_icon_value( $content['icon'] ?? '', '' ),
                    'view'       => in_array( $content['view'] ?? 'default', $allowed_views, true ) ? $content['view'] : 'default',
                    'title'      => sanitize_text_field( $content['title'] ?? '' ),
                    'text'       => wp_kses_post( $content['text'] ?? '' ),
                    'linkUrl'    => self::sanitize_url( $content['linkUrl'] ?? '' ),
                    'linkNewTab' => (bool) ( $content['linkNewTab'] ?? false ),
                ];
            }

            // ── CTA ───────────────────────────────────────────────────────────
            case 'cta':
                return [
                    'title'         => sanitize_text_field( $content['title']         ?? '' ),
                    'subtitle'      => wp_kses_post( $content['subtitle']             ?? '' ),
                    'primaryText'   => sanitize_text_field( $content['primaryText']   ?? '' ),
                    'primaryUrl'    => self::sanitize_url( $content['primaryUrl']     ?? '#' ),
                    'secondaryText' => sanitize_text_field( $content['secondaryText'] ?? '' ),
                    'secondaryUrl'  => self::sanitize_url( $content['secondaryUrl']   ?? '#' ),
                ];

            // ── Stats Card ────────────────────────────────────────────────────
            case 'stats-card':
                return [
                    'value'    => sanitize_text_field( $content['value']    ?? '' ),
                    'label'    => sanitize_text_field( $content['label']    ?? '' ),
                    'sublabel' => sanitize_text_field( $content['sublabel'] ?? '' ),
                ];

            // ── Timeline ──────────────────────────────────────────────────────
            case 'timeline': {
                $items = [];
                foreach ( (array) ( $content['items'] ?? [] ) as $item ) {
                    if ( ! is_array( $item ) ) continue;
                    $items[] = [
                        'date'  => sanitize_text_field( $item['date']  ?? '' ),
                        'title' => sanitize_text_field( $item['title'] ?? '' ),
                        'text'  => wp_kses_post( $item['text'] ?? '' ),
                    ];
                }
                return [ 'items' => $items ];
            }

            // ── Testimonials carousel ─────────────────────────────────────────
            case 'testimonials': {
                $layout = in_array( $content['layout'] ?? 'grid', [ 'carousel', 'grid' ], true )
                          ? $content['layout'] : 'grid';
                $cols   = max( 1, min( 4, intval( $content['cols'] ?? 3 ) ) );
                $items  = [];
                foreach ( (array) ( $content['items'] ?? [] ) as $item ) {
                    if ( ! is_array( $item ) ) continue;
                    $items[] = [
                        'text'      => wp_kses_post( $item['text']      ?? '' ),
                        'name'      => sanitize_text_field( $item['name']      ?? '' ),
                        'role'      => sanitize_text_field( $item['role']      ?? '' ),
                        'avatarUrl' => self::sanitize_url( $item['avatarUrl']  ?? '' ),
                        'rating'    => min( 5, max( 1, intval( $item['rating'] ?? 5 ) ) ),
                    ];
                }
                $autoplay = max( 0, intval( $content['autoplay'] ?? 0 ) );
                return [ 'layout' => $layout, 'cols' => $cols, 'autoplay' => $autoplay, 'items' => $items ];
            }

            case 'tabs': {
                $tab_style_raw = $content['tabStyle'] ?? 'underline';
                $tab_style = in_array( $tab_style_raw, [ 'underline','boxed','pill' ], true )
                             ? $tab_style_raw : 'underline';
                $items = [];
                foreach ( (array) ( $content['items'] ?? [] ) as $item ) {
                    if ( ! is_array( $item ) ) continue;
                    $items[] = [
                        'label'   => sanitize_text_field( $item['label']   ?? '' ),
                        'content' => wp_kses_post(        $item['content'] ?? '' ),
                    ];
                }
                return [ 'tabStyle' => $tab_style, 'items' => $items ];
            }

            case 'tabs-adv': {
                // Tab METADATA only (title/icon/cssId). The per-tab widgets live
                // in this node's `children` (container panels) and are sanitized
                // by the recursive child pass — not here.
                $tab_style_raw = $content['tabStyle'] ?? 'underline';
                $tab_style = in_array( $tab_style_raw, [ 'underline','boxed','pill' ], true )
                             ? $tab_style_raw : 'underline';
                $items = [];
                foreach ( (array) ( $content['items'] ?? [] ) as $item ) {
                    if ( ! is_array( $item ) ) continue;
                    $clean = [
                        'id'    => sanitize_html_class( $item['id']    ?? '' ),
                        'title' => sanitize_text_field( $item['title'] ?? '' ),
                        'cssId' => sanitize_html_class( $item['cssId'] ?? '' ),
                    ];
                    if ( isset( $item['icon'] ) && '' !== $item['icon'] && [] !== $item['icon'] ) {
                        $icon = self::sanitize_icon_value( $item['icon'], '' );
                        if ( '' !== $icon && [] !== $icon ) {
                            $clean['icon'] = $icon;
                        }
                    }
                    $items[] = $clean;
                }
                $active = isset( $content['activeTab'] ) ? max( 0, (int) $content['activeTab'] ) : 0;
                return [ 'tabStyle' => $tab_style, 'items' => $items, 'activeTab' => $active ];
            }

            case 'menu': {
                $layout = in_array( $content['layout'] ?? 'horizontal', [ 'horizontal', 'vertical' ], true )
                          ? $content['layout'] : 'horizontal';

                $align = in_array( $content['align'] ?? 'left', [ 'left', 'center', 'right', 'space-between' ], true )
                         ? $content['align'] : 'left';

                $responsive = in_array( $content['responsiveMenu'] ?? 'dropdown', [ 'dropdown', 'none' ], true )
                              ? $content['responsiveMenu'] : 'dropdown';

                $sub_icon = in_array( $content['submenuIcon'] ?? 'chevron', [ 'chevron', 'caret', 'plus', 'none' ], true )
                            ? $content['submenuIcon'] : 'chevron';

                $trigger = in_array( $content['dropdownTrigger'] ?? 'hover', [ 'hover', 'click' ], true )
                           ? $content['dropdownTrigger'] : 'hover';

                return [
                    'menuId'           => max( 0, (int) ( $content['menuId'] ?? 0 ) ),
                    'menuLocation'     => sanitize_key( $content['menuLocation'] ?? '' ),
                    // Demo labels shown ONLY in a design preview; the live menu
                    // is used on real pages. See Jalpi_Element_Menu::get_items().
                    'demoItems'        => self::sanitize_demo_menu_items( $content['demoItems'] ?? [] ),
                    'layout'           => $layout,
                    'submenuIcon'      => $sub_icon,
                    'showSubmenus'     => ! isset( $content['showSubmenus'] ) || ! empty( $content['showSubmenus'] ),
                    'dropdownTrigger'  => $trigger,
                    'responsiveMenu'   => $responsive,
                    'mobileBreakpoint' => max( 320, min( 1440, (int) ( $content['mobileBreakpoint'] ?? 768 ) ) ),
                    'align'            => $align,
                    'menuWidth'        => sanitize_text_field( $content['menuWidth'] ?? '' ),
                    'itemSpacing'      => sanitize_text_field( $content['itemSpacing'] ?? '' ),
                    'ariaLabel'        => sanitize_text_field( $content['ariaLabel'] ?? '' ),
                ];
            }

            case 'adv-menu': {
                // Mega-panel WIDGETS live in this node's `children` (container
                // panels) and are sanitized by the recursive child pass. What is
                // stored here is only the menu configuration plus the map from a
                // WordPress menu-item id to its panel.
                $pick = static function ( $value, array $allowed, string $fallback ): string {
                    $value = is_string( $value ) ? trim( $value ) : '';
                    return in_array( $value, $allowed, true ) ? $value : $fallback;
                };

                $mega = [];
                if ( isset( $content['megaItems'] ) && is_array( $content['megaItems'] ) ) {
                    foreach ( $content['megaItems'] as $item_id => $cfg ) {
                        $item_id = (int) $item_id;
                        if ( $item_id <= 0 || ! is_array( $cfg ) ) {
                            continue;
                        }
                        $mega[ $item_id ] = [
                            'enabled'    => ! empty( $cfg['enabled'] ),
                            'cols'       => max( 1, min( 12, (int) ( $cfg['cols'] ?? 3 ) ) ),
                            // -1 marks "no panel yet"; the element skips those.
                            'panelIndex' => max( -1, (int) ( $cfg['panelIndex'] ?? -1 ) ),
                        ];
                    }
                }

                return [
                    'menuId'             => max( 0, (int) ( $content['menuId'] ?? 0 ) ),
                    'menuLocation'       => sanitize_key( $content['menuLocation'] ?? '' ),
                    'layout'             => $pick( $content['layout'] ?? '', [ 'horizontal', 'vertical' ], 'horizontal' ),
                    'megaTrigger'        => $pick( $content['megaTrigger'] ?? '', [ 'hover', 'click' ], 'hover' ),
                    'panelWidth'         => $pick( $content['panelWidth'] ?? '', [ 'container', 'full', 'custom' ], 'container' ),
                    'panelCustomWidth'   => sanitize_text_field( $content['panelCustomWidth'] ?? '' ),
                    'dropdownPosition'   => $pick( $content['dropdownPosition'] ?? '', [ 'left', 'center', 'right' ], 'left' ),
                    'align'              => $pick( $content['align'] ?? '', [ 'left', 'center', 'right', 'justify' ], 'left' ),
                    'submenuIndicator'   => $pick( $content['submenuIndicator'] ?? '', [ 'arrow', 'plus', 'none' ], 'arrow' ),
                    'animation'          => $pick( $content['animation'] ?? '', [ 'fade', 'slide-down', 'slide-up', 'zoom', 'scale', 'none' ], 'fade' ),
                    'animationDuration'  => max( 0, min( 3000, (int) ( $content['animationDuration'] ?? 250 ) ) ),
                    'openDelay'          => max( 0, min( 2000, (int) ( $content['openDelay'] ?? 100 ) ) ),
                    'closeDelay'         => max( 0, min( 2000, (int) ( $content['closeDelay'] ?? 200 ) ) ),
                    'breakpoint'         => max( 320, min( 1440, (int) ( $content['breakpoint'] ?? 1024 ) ) ),
                    'mobileLayout'       => $pick( $content['mobileLayout'] ?? '', [ 'accordion', 'offcanvas', 'dropdown' ], 'accordion' ),
                    'stickyMobileHeader' => ! empty( $content['stickyMobileHeader'] ),
                    'menuWidth'          => sanitize_text_field( $content['menuWidth'] ?? '' ),
                    'itemSpacing'        => sanitize_text_field( $content['itemSpacing'] ?? '' ),
                    'ariaLabel'          => sanitize_text_field( $content['ariaLabel'] ?? '' ),
                    'megaItems'          => $mega,
                ];
            }

            case 'breadcrumbs': {
                $sep = (string) ( $content['separator'] ?? 'chevron' );
                if ( ! in_array( $sep, [ 'chevron', 'slash', 'arrow', 'dot', 'pipe' ], true ) ) {
                    $sep = 'chevron';
                }
                return [
                    'separator'   => $sep,
                    'homeLabel'   => sanitize_text_field( $content['homeLabel'] ?? '' ),
                    'showHome'    => ! isset( $content['showHome'] ) || ! empty( $content['showHome'] ),
                    'showCurrent' => ! isset( $content['showCurrent'] ) || ! empty( $content['showCurrent'] ),
                    'ariaLabel'   => sanitize_text_field( $content['ariaLabel'] ?? '' ),
                ];
            }

            case 'image-box':
                return [
                    'url'           => self::sanitize_url(   $content['url']           ?? '' ),
                    'alt'           => sanitize_text_field(  $content['alt']           ?? '' ),
                    'title'         => sanitize_text_field(  $content['title']         ?? '' ),
                    'description'   => wp_kses_post(         $content['description']   ?? '' ),
                    'titleTag'      => in_array( $content['titleTag'] ?? 'h3', [ 'h2','h3','h4','h5','h6' ], true )
                                       ? $content['titleTag'] : 'h3',
                    'imagePosition' => in_array( $content['imagePosition'] ?? 'top', [ 'top','left','right' ], true )
                                       ? $content['imagePosition'] : 'top',
                    'linkUrl'       => self::sanitize_url(   $content['linkUrl']       ?? '' ),
                ];

            case 'social-icons': {
                $items = [];
                foreach ( (array) ( $content['items'] ?? [] ) as $item ) {
                    if ( ! is_array( $item ) ) continue;
                    $items[] = [
                        'platform' => sanitize_key( $item['platform'] ?? 'facebook' ),
                        'url'      => self::sanitize_url( $item['url'] ?? '' ),
                    ];
                }
                return [
                    'iconStyle' => in_array( $content['iconStyle'] ?? 'circle', [ 'circle','square','minimal' ], true )
                                   ? $content['iconStyle'] : 'circle',
                    'iconSize'  => self::sanitize_dimension( $content['iconSize'] ?? '40px' ) ?: '40px',
                    'align'     => in_array( $content['align'] ?? 'center', [ 'left','center','right' ], true )
                                   ? $content['align'] : 'center',
                    'color'     => self::sanitize_color( $content['color'] ?? '' ),
                    'items'     => $items,
                ];
            }

            // ── WooCommerce Single Product ───────────────────────────────────
            case 'single-product': {
                $enum = static fn( $val, array $allowed, string $default ): string =>
                    in_array( $val, $allowed, true ) ? (string) $val : $default;

                $bool = static fn( string $k, bool $d ): bool =>
                    array_key_exists( $k, $content ) ? ! empty( $content[ $k ] ) : $d;

                // Share networks: comma list restricted to the known set.
                $allowed_nets = [ 'facebook', 'twitter', 'linkedin', 'pinterest', 'whatsapp' ];
                $nets = preg_split( '/\s*,\s*/', (string) ( $content['shareNetworks'] ?? '' ), -1, PREG_SPLIT_NO_EMPTY ) ?: [];
                $nets = array_values( array_intersect( array_map( 'sanitize_key', $nets ), $allowed_nets ) );

                return [
                    // Source
                    'source'          => $enum( $content['source'] ?? '', [ 'current', 'select', 'custom' ], 'current' ),
                    'productId'       => max( 0, (int) ( $content['productId']       ?? 0 ) ),
                    'customProductId' => max( 0, (int) ( $content['customProductId'] ?? 0 ) ),
                    'previewProduct'  => max( 0, (int) ( $content['previewProduct']  ?? 0 ) ),

                    // Layout / structure
                    'layout'          => $enum( $content['layout'] ?? '', [ 'default', 'modern', 'classic', 'custom' ], 'default' ),
                    'galleryPosition' => $enum( $content['galleryPosition'] ?? '', [ 'left', 'right', 'top' ], 'left' ),
                    'thumbPosition'   => $enum( $content['thumbPosition']   ?? '', [ 'vertical', 'horizontal' ], 'horizontal' ),
                    'imageLink'       => $enum( $content['imageLink']       ?? '', [ 'lightbox', 'product', 'none' ], 'lightbox' ),
                    'titleTag'        => $enum( $content['titleTag']        ?? '', [ 'h1', 'h2', 'h3', 'div' ], 'h1' ),

                    'showGallery'   => $bool( 'showGallery',   true ),
                    'showTitle'     => $bool( 'showTitle',     true ),
                    'showRating'    => $bool( 'showRating',    true ),
                    'showPrice'     => $bool( 'showPrice',     true ),
                    'showExcerpt'   => $bool( 'showExcerpt',   true ),
                    'showAddToCart' => $bool( 'showAddToCart', true ),
                    'showMeta'      => $bool( 'showMeta',      true ),
                    'showShare'     => $bool( 'showShare',     false ),
                    'showTabs'      => $bool( 'showTabs',      true ),
                    'showRelated'   => $bool( 'showRelated',   true ),
                    'showUpsells'   => $bool( 'showUpsells',   false ),

                    // Gallery
                    'showThumbs' => $bool( 'showThumbs', true ),
                    'lazyLoad'   => $bool( 'lazyLoad',   true ),

                    // Rating
                    'showReviewCount' => $bool( 'showReviewCount', true ),
                    'linkToReviews'   => $bool( 'linkToReviews',   false ),
                    'showEmptyRating' => $bool( 'showEmptyRating', false ),

                    // Price
                    'showRegularPrice' => $bool( 'showRegularPrice', true ),
                    'showSalePrice'    => $bool( 'showSalePrice',    true ),
                    'showSaleBadge'    => $bool( 'showSaleBadge',    true ),

                    // Cart
                    'showQuantity'  => $bool( 'showQuantity', true ),
                    'quantityType'  => $enum( $content['quantityType'] ?? '', [ 'input', 'plusminus' ], 'input' ),
                    'buttonText'    => sanitize_text_field( (string) ( $content['buttonText'] ?? '' ) ),
                    'ajaxAddToCart' => $bool( 'ajaxAddToCart', true ),

                    // Meta
                    'metaSku'        => $bool( 'metaSku',        true ),
                    'metaCategories' => $bool( 'metaCategories', true ),
                    'metaTags'       => $bool( 'metaTags',       true ),
                    'metaSeparator'  => sanitize_text_field( (string) ( $content['metaSeparator'] ?? '' ) ),
                    'metaShowLabels' => $bool( 'metaShowLabels', true ),

                    // Share
                    'shareNetworks' => implode( ',', $nets ),

                    // Tabs
                    'defaultTab'  => $enum( $content['defaultTab'] ?? '',
                        [ 'description', 'additional_information', 'reviews' ], 'description' ),
                    'tabPosition' => $enum( $content['tabPosition'] ?? '', [ 'top', 'left' ], 'top' ),

                    // Related / upsells
                    'relatedCount'   => max( 1, min( 24, (int) ( $content['relatedCount']   ?? 4 ) ) ),
                    'relatedColumns' => max( 1, min( 6,  (int) ( $content['relatedColumns'] ?? 4 ) ) ),
                    'relatedOrderBy' => $enum( $content['relatedOrderBy'] ?? '',
                        [ 'rand', 'date', 'price', 'title', 'popularity' ], 'rand' ),
                    'upsellsCount'   => max( 1, min( 24, (int) ( $content['upsellsCount']   ?? 4 ) ) ),
                    'upsellsColumns' => max( 1, min( 6,  (int) ( $content['upsellsColumns'] ?? 4 ) ) ),

                    // Empty state
                    'emptyMessage' => sanitize_text_field( (string) ( $content['emptyMessage'] ?? '' ) ),
                ];
            }

            // ── WooCommerce Product Categories ───────────────────────────────
            case 'product-categories': {
                $enum = static fn( $val, array $allowed, string $default ): string =>
                    in_array( $val, $allowed, true ) ? (string) $val : $default;

                // Term-ID lists feed include/exclude — digits only.
                $idlist = static function ( $val ): string {
                    $parts = preg_split( '/[\s,]+/', (string) $val, -1, PREG_SPLIT_NO_EMPTY ) ?: [];
                    return implode( ',', array_filter( array_map( 'absint', $parts ) ) );
                };

                $bool = static fn( string $k, bool $d ): bool =>
                    array_key_exists( $k, $content ) ? ! empty( $content[ $k ] ) : $d;

                return [
                    // Layout
                    'skin'        => $enum( $content['skin']   ?? '', [ 'classic', 'cards', 'overlay', 'minimal' ], 'classic' ),
                    'layout'      => $enum( $content['layout'] ?? '', [ 'grid', 'carousel', 'list' ], 'grid' ),
                    'equalHeight' => $bool( 'equalHeight', true ),

                    // Query
                    'source' => $enum( $content['source'] ?? '',
                        [ 'all', 'selected', 'parent', 'child', 'featured' ], 'all' ),
                    'includeCategories' => $idlist( $content['includeCategories'] ?? '' ),
                    'excludeCategories' => $idlist( $content['excludeCategories'] ?? '' ),
                    'categoryIds'       => $idlist( $content['categoryIds']       ?? '' ),
                    'parentCategory'    => max( 0, (int) ( $content['parentCategory'] ?? 0 ) ),
                    'hideEmpty'         => ! empty( $content['hideEmpty'] ),
                    'number'            => max( 0, min( 100, (int) ( $content['number'] ?? 12 ) ) ),
                    'orderBy'           => $enum( $content['orderBy'] ?? '',
                        [ 'name', 'id', 'count', 'menu_order', 'slug' ], 'name' ),
                    'order'             => $enum( strtoupper( (string) ( $content['order'] ?? '' ) ),
                        [ 'ASC', 'DESC' ], 'ASC' ),

                    // Category content
                    'showImage'       => $bool( 'showImage',       true ),
                    'showName'        => $bool( 'showName',        true ),
                    'showCount'       => $bool( 'showCount',       true ),
                    'showDescription' => $bool( 'showDescription', false ),
                    'showButton'      => $bool( 'showButton',      false ),

                    // Image + button behaviour
                    'lazyLoad'      => $bool( 'lazyLoad', true ),
                    'buttonText'    => sanitize_text_field( (string) ( $content['buttonText'] ?? '' ) ),
                    'openNewWindow' => ! empty( $content['openNewWindow'] ),

                    // Carousel
                    'slidesPerView'  => max( 1, min( 6,      (int) ( $content['slidesPerView']  ?? 4 ) ) ),
                    'slidesToScroll' => max( 1, min( 6,      (int) ( $content['slidesToScroll'] ?? 1 ) ) ),
                    'carouselLoop'   => $bool( 'carouselLoop', true ),
                    'autoplay'       => max( 0, min( 30000,  (int) ( $content['autoplay'] ?? 0 ) ) ),
                    'carouselSpeed'  => max( 100, min( 5000, (int) ( $content['carouselSpeed'] ?? 400 ) ) ),
                    'pauseOnHover'   => $bool( 'pauseOnHover', true ),
                    'showArrows'     => $bool( 'showArrows',   true ),
                    'showDots'       => $bool( 'showDots',     true ),

                    // Empty state
                    'emptyMessage' => sanitize_text_field( (string) ( $content['emptyMessage'] ?? '' ) ),
                    'emptyIcon'    => sanitize_text_field( (string) ( $content['emptyIcon']    ?? '' ) ),
                ];
            }

            case 'products-grid': {
                $enum = static fn( $val, array $allowed, string $default ): string =>
                    in_array( $val, $allowed, true ) ? (string) $val : $default;

                // ID lists are stored as text but must only ever contain digits,
                // commas and spaces — they feed post__in / post__not_in.
                $idlist = static function ( $val ): string {
                    $parts = preg_split( '/[\s,]+/', (string) $val, -1, PREG_SPLIT_NO_EMPTY ) ?: [];
                    $ids   = array_filter( array_map( 'absint', $parts ) );
                    return implode( ',', $ids );
                };

                // Term lists accept slugs or IDs; sanitize_title() each entry.
                $termlist = static function ( $val ): string {
                    $parts = preg_split( '/\s*,\s*/', (string) $val, -1, PREG_SPLIT_NO_EMPTY ) ?: [];
                    $out   = [];
                    foreach ( $parts as $p ) {
                        $p = trim( (string) $p );
                        if ( '' === $p ) continue;
                        $out[] = is_numeric( $p ) ? (string) absint( $p ) : sanitize_title( $p );
                    }
                    return implode( ',', array_filter( $out ) );
                };

                $bool = static fn( string $k, bool $d ): bool =>
                    array_key_exists( $k, $content ) ? ! empty( $content[ $k ] ) : $d;

                return [
                    // Layout
                    'skin'        => $enum( $content['skin'] ?? '', [ 'classic', 'cards', 'minimal' ], 'classic' ),
                    'perPage'     => max( 1, min( 100, (int) ( $content['perPage'] ?? 8 ) ) ),
                    'rows'        => max( 1, min( 20,  (int) ( $content['rows']    ?? 1 ) ) ),
                    'useRows'     => ! empty( $content['useRows'] ),
                    'equalHeight' => $bool( 'equalHeight', true ),
                    'masonry'     => ! empty( $content['masonry'] ),

                    // Query
                    'source' => $enum( $content['source'] ?? '', [
                        'latest', 'featured', 'sale', 'best-selling', 'top-rated',
                        'related', 'upsells', 'cross-sells', 'manual',
                    ], 'latest' ),
                    'categories'      => $termlist( $content['categories']      ?? '' ),
                    'tags'            => $termlist( $content['tags']            ?? '' ),
                    'brands'          => $termlist( $content['brands']          ?? '' ),
                    'includeProducts' => $idlist(   $content['includeProducts'] ?? '' ),
                    'excludeProducts' => $idlist(   $content['excludeProducts'] ?? '' ),
                    'includeIds'      => $idlist(   $content['includeIds']      ?? '' ),
                    'excludeIds'      => $idlist(   $content['excludeIds']      ?? '' ),
                    'stockStatus'     => $enum( $content['stockStatus'] ?? '',
                        [ 'any', 'instock', 'outofstock', 'onbackorder' ], 'any' ),
                    'productStatus'   => $enum( $content['productStatus'] ?? '',
                        [ 'publish', 'private', 'any' ], 'publish' ),
                    'orderBy'         => $enum( $content['orderBy'] ?? '', [
                        'date', 'title', 'price', 'popularity', 'rating',
                        'menu_order', 'rand', 'modified',
                    ], 'date' ),
                    'order'           => $enum( strtoupper( (string) ( $content['order'] ?? '' ) ),
                        [ 'ASC', 'DESC' ], 'DESC' ),
                    'offset'          => max( 0, min( 10000, (int) ( $content['offset'] ?? 0 ) ) ),
                    'ignoreSticky'    => ! empty( $content['ignoreSticky'] ),

                    // Product elements
                    'showImage'      => $bool( 'showImage',      true ),
                    'showSaleBadge'  => $bool( 'showSaleBadge',  true ),
                    'showTitle'      => $bool( 'showTitle',      true ),
                    'showRating'     => $bool( 'showRating',     true ),
                    'showPrice'      => $bool( 'showPrice',      true ),
                    'showExcerpt'    => $bool( 'showExcerpt',    false ),
                    'showAddToCart'  => $bool( 'showAddToCart',  true ),
                    'showWishlist'   => $bool( 'showWishlist',   false ),
                    'showCompare'    => $bool( 'showCompare',    false ),
                    'showQuickView'  => $bool( 'showQuickView',  false ),
                    'showCategories' => $bool( 'showCategories', false ),
                    'showSku'        => $bool( 'showSku',        false ),

                    // Image / button behaviour
                    'secondaryImage' => ! empty( $content['secondaryImage'] ),
                    'lazyLoad'       => $bool( 'lazyLoad',      true ),
                    'buttonText'     => sanitize_text_field( (string) ( $content['buttonText'] ?? '' ) ),
                    'showQuantity'   => ! empty( $content['showQuantity'] ),
                    'ajaxAddToCart'  => $bool( 'ajaxAddToCart', true ),

                    // Pagination + empty state
                    'pagination'   => $enum( $content['pagination'] ?? '',
                        [ 'none', 'numbers', 'prev-next', 'load-more', 'infinite' ], 'none' ),
                    'loadMoreText' => sanitize_text_field( (string) ( $content['loadMoreText'] ?? '' ) ),
                    'emptyMessage' => sanitize_text_field( (string) ( $content['emptyMessage'] ?? '' ) ),
                    'emptyIcon'    => sanitize_text_field( (string) ( $content['emptyIcon']    ?? '' ) ),
                ];
            }

            case 'gallery': {
                $items = [];
                foreach ( (array) ( $content['items'] ?? [] ) as $item ) {
                    if ( ! is_array( $item ) ) continue;
                    $items[] = [
                        'id'         => sanitize_html_class( $item['id']         ?? '' ),
                        'url'        => self::sanitize_url(  $item['url']        ?? '' ),
                        'alt'        => sanitize_text_field( $item['alt']        ?? '' ),
                        'caption'    => sanitize_text_field( $item['caption']    ?? '' ),
                        'linkUrl'    => self::sanitize_url(  $item['linkUrl']    ?? '' ),
                        'linkTarget' => ( ($item['linkTarget'] ?? '_blank') === '_self' ) ? '_self' : '_blank',
                        'category'   => sanitize_html_class( $item['category']   ?? '' ),
                    ];
                }
                $layout = in_array( $content['layout'] ?? 'grid', [ 'grid', 'masonry' ], true ) ? $content['layout'] : 'grid';
                return [
                    'layout'         => $layout,
                    'enableLightbox' => ! empty( $content['enableLightbox'] ),
                    'enableFilter'   => ! empty( $content['enableFilter'] ),
                    'items'          => $items,
                ];
            }

            case 'image-carousel': {
                $items = [];
                foreach ( (array) ( $content['items'] ?? [] ) as $item ) {
                    if ( ! is_array( $item ) ) continue;
                    $items[] = [
                        'url'         => self::sanitize_url(  $item['url']         ?? '' ),
                        'alt'         => sanitize_text_field( $item['alt']         ?? '' ),
                        'caption'     => sanitize_text_field( $item['caption']     ?? '' ),
                        'description' => sanitize_text_field( $item['description'] ?? '' ),
                        'linkUrl'     => self::sanitize_url(  $item['linkUrl']     ?? '' ),
                        'linkNewTab'  => ! empty( $item['linkNewTab'] ),
                    ];
                }
                return [ 'items' => $items ];
            }

            case 'countdown':
                return [
                    'targetDate' => sanitize_text_field( $content['targetDate'] ?? '' ),
                    'label'      => sanitize_text_field( $content['label']      ?? '' ),
                ];

            case 'flip-box':
                return [
                    'frontTitle'       => sanitize_text_field( $content['frontTitle']       ?? '' ),
                    'frontDescription' => sanitize_text_field( $content['frontDescription'] ?? '' ),
                    'frontBg'          => self::sanitize_color( $content['frontBg']         ?? '#6366f1' ) ?: '#6366f1',
                    'frontColor'       => self::sanitize_color( $content['frontColor']      ?? '#ffffff' ) ?: '#ffffff',
                    'backTitle'        => sanitize_text_field( $content['backTitle']        ?? '' ),
                    'backDescription'  => sanitize_text_field( $content['backDescription']  ?? '' ),
                    'backBg'           => self::sanitize_color( $content['backBg']          ?? '#111827' ) ?: '#111827',
                    'backColor'        => self::sanitize_color( $content['backColor']       ?? '#ffffff' ) ?: '#ffffff',
                    'btnText'          => sanitize_text_field( $content['btnText']          ?? '' ),
                    'btnUrl'           => self::sanitize_url(  $content['btnUrl']           ?? '#' ),
                    'height'           => self::sanitize_dimension( $content['height']      ?? '220px' ) ?: '220px',
                ];

            case 'star-rating':
                return [
                    'rating'    => max( 0, intval( $content['rating']    ?? 4 ) ),
                    'maxRating' => max( 1, intval( $content['maxRating'] ?? 5 ) ),
                    'label'     => sanitize_text_field( $content['label']     ?? '' ),
                    'starColor' => self::sanitize_color( $content['starColor'] ?? '#f59e0b' ) ?: '#f59e0b',
                    'size'      => self::sanitize_dimension( $content['size'] ?? '24px' ) ?: '24px',
                    'align'     => in_array( $content['align'] ?? 'left', [ 'left','center','right' ], true )
                                   ? $content['align'] : 'left',
                ];

            default:
                return self::sanitize_generic_content( $type, $content );
        }
    }

    /**
     * Sanitize content for a schema-driven widget by its field schema (from
     * includes/widgets-schema.php). Each field is escaped according to its
     * declared control type. Types not in the schema return [] exactly as the
     * old `default` branch did — so genuinely-unknown types stay stripped.
     */
    private static function sanitize_generic_content( string $type, array $content ): array {
        if ( ! function_exists( 'jalpi_widget_schemas' ) ) {
            return [];
        }
        $schemas = jalpi_widget_schemas();
        if ( ! isset( $schemas[ $type ]['fields'] ) ) {
            return [];
        }

        $clean = [];
        foreach ( $schemas[ $type ]['fields'] as $key => $control ) {
            if ( ! isset( $content[ $key ] ) ) {
                continue;
            }
            $raw = $content[ $key ];
            switch ( $control ) {
                case 'textarea':
                    $clean[ $key ] = wp_kses_post( (string) $raw );
                    break;
                case 'number':
                    $clean[ $key ] = self::sanitize_dimension( (string) $raw );
                    break;
                case 'toggle':
                    $clean[ $key ] = (bool) $raw;
                    break;
                case 'color':
                    $clean[ $key ] = self::sanitize_color( (string) $raw );
                    break;
                case 'url':
                    $clean[ $key ] = self::sanitize_url( (string) $raw );
                    break;
                case 'icon':
                    // Icon value is an IconValue array (or legacy string) — reuse
                    // the same sanitizer the bespoke icon widgets use, so the
                    // full icon library works here identically and safely.
                    $clean[ $key ] = self::sanitize_icon_value( $raw, '' );
                    break;
                case 'social-items':
                    // JSON list of { icon, link, color } — the icon+link repeater
                    // (Social Share). Decode, sanitize each item with the same
                    // helpers used elsewhere, re-encode as JSON.
                    $decoded = is_array( $raw ) ? $raw : json_decode( (string) $raw, true );
                    $out     = [];
                    if ( is_array( $decoded ) ) {
                        foreach ( $decoded as $item ) {
                            if ( ! is_array( $item ) ) {
                                continue;
                            }
                            $out[] = [
                                'icon'  => self::sanitize_icon_value( $item['icon'] ?? '', '' ),
                                'link'  => self::sanitize_url( (string) ( $item['link'] ?? '' ) ),
                                'color' => self::sanitize_color( (string) ( $item['color'] ?? '' ) ),
                            ];
                        }
                    }
                    $clean[ $key ] = wp_json_encode( $out );
                    break;
                case 'multiicon':
                case 'select':
                case 'text':
                default:
                    $clean[ $key ] = sanitize_text_field( (string) $raw );
                    break;
            }
        }
        return $clean;
    }

    // -------------------------------------------------------------------------
    // Global styles sanitizer
    // -------------------------------------------------------------------------

    private static function sanitize_global_styles( array $gs ): array {
        $clean = [];

        // Colors
        if ( isset( $gs['colors'] ) && is_array( $gs['colors'] ) ) {
            $colors = [];
            foreach ( $gs['colors'] as $c ) {
                if ( ! is_array( $c ) ) continue;
                $val = self::sanitize_color( $c['value'] ?? '' );
                if ( $val ) {
                    $colors[] = [
                        'id'    => sanitize_key( $c['id']   ?? '' ),
                        'name'  => sanitize_text_field( $c['name'] ?? '' ),
                        'value' => $val,
                    ];
                }
            }
            $clean['colors'] = $colors;
        }

        if ( isset( $gs['typography'] ) && is_array( $gs['typography'] ) ) {
            $clean['typography'] = self::sanitize_typography_presets( $gs['typography'] );
        }

        if ( isset( $gs['buttons'] ) && is_array( $gs['buttons'] ) ) {
            $clean['buttons'] = self::sanitize_button_presets( $gs['buttons'] );
        }

        // Spacing scale
        if ( isset( $gs['spacing'] ) && is_array( $gs['spacing'] ) ) {
            $sp = [];
            foreach ( [ 'xs','sm','md','lg','xl','2xl' ] as $k ) {
                if ( ! empty( $gs['spacing'][ $k ] ) ) {
                    $d = self::sanitize_dimension( $gs['spacing'][ $k ] );
                    if ( '' !== $d ) $sp[ $k ] = $d;
                }
            }
            $clean['spacing'] = $sp;
        }

        return $clean;
    }

    /**
     * Sanitize a list of Typography composite presets. Extracted verbatim from
     * sanitize_global_styles() so both the legacy per-page path and the new
     * site-wide Design System path (sanitize_design_system()) share identical,
     * already-battle-tested behavior.
     */
    private static function sanitize_typography_presets( array $list ): array {
        $typo = [];
        foreach ( $list as $t ) {
            if ( ! is_array( $t ) ) continue;
            $preset = [ 'id' => sanitize_key( $t['id'] ?? '' ), 'name' => sanitize_text_field( $t['name'] ?? '' ) ];
            foreach ( [ 'fontSize','lineHeight','letterSpacing' ] as $dk ) {
                if ( ! empty( $t[ $dk ] ) ) { $d = self::sanitize_dimension( $t[ $dk ] ); if ( '' !== $d ) $preset[ $dk ] = $d; }
            }
            foreach ( [ 'color' ] as $ck ) {
                if ( ! empty( $t[ $ck ] ) ) { $c = self::sanitize_color( $t[ $ck ] ); if ( $c ) $preset[ $ck ] = $c; }
            }
            foreach ( [ 'fontFamily','fontWeight','textTransform','textDecoration' ] as $tk ) {
                if ( ! empty( $t[ $tk ] ) ) $preset[ $tk ] = sanitize_text_field( $t[ $tk ] );
            }
            if ( $preset['id'] ) $typo[] = $preset;
        }
        return $typo;
    }

    /**
     * Sanitize a list of Button composite presets. Extracted verbatim from
     * sanitize_global_styles() — see sanitize_typography_presets() docblock.
     */
    private static function sanitize_button_presets( array $list ): array {
        $btns = [];
        foreach ( $list as $b ) {
            if ( ! is_array( $b ) ) continue;
            $preset = [ 'id' => sanitize_key( $b['id'] ?? '' ), 'name' => sanitize_text_field( $b['name'] ?? '' ) ];
            foreach ( [ 'backgroundColor','color','border','boxShadow' ] as $ck ) {
                if ( ! empty( $b[ $ck ] ) ) $preset[ $ck ] = self::sanitize_css_value( $b[ $ck ] );
            }
            foreach ( [ 'borderRadius','paddingTop','paddingBottom','paddingLeft','paddingRight','fontSize' ] as $dk ) {
                if ( ! empty( $b[ $dk ] ) ) { $d = self::sanitize_dimension( $b[ $dk ] ); if ( '' !== $d ) $preset[ $dk ] = $d; }
            }
            foreach ( [ 'fontWeight','textTransform','fontFamily' ] as $tk ) {
                if ( ! empty( $b[ $tk ] ) ) $preset[ $tk ] = sanitize_text_field( $b[ $tk ] );
            }
            if ( $preset['id'] ) $btns[] = $preset;
        }
        return $btns;
    }

    // -------------------------------------------------------------------------
    // Design System sanitizer (site-wide token set)
    // -------------------------------------------------------------------------

    /**
     * Sanitize a full site-wide Design System token set: 8 generic
     * {id,name,value,tabletValue?,mobileValue?} token categories plus the
     * existing typography/button composite preset lists (unchanged shape,
     * reused via the shared helpers above).
     *
     * @param array $ds Raw (untrusted) design system data.
     * @return array    Clean, safe design system data.
     */
    public static function sanitize_design_system( array $ds ): array {
        $clean = [];

        foreach ( Jalpi_Settings::TOKEN_CATEGORIES as $category => $list_key ) {
            if ( ! isset( $ds[ $list_key ] ) || ! is_array( $ds[ $list_key ] ) ) continue;
            $clean[ $list_key ] = self::sanitize_token_list( $ds[ $list_key ], self::token_value_sanitizer( $category ) );
        }

        if ( isset( $ds['typography'] ) && is_array( $ds['typography'] ) ) {
            $clean['typography'] = self::sanitize_typography_presets( $ds['typography'] );
        }
        if ( isset( $ds['buttons'] ) && is_array( $ds['buttons'] ) ) {
            $clean['buttons'] = self::sanitize_button_presets( $ds['buttons'] );
        }

        return $clean;
    }

    /**
     * Generic sanitizer for one Design System token category — a single
     * implementation shared by all 8 generic categories (colors, fontFamilies,
     * fontSizes, lineHeights, letterSpacing, borderRadius, boxShadows, spacing,
     * containerWidths) instead of 8 near-duplicate loops. Tolerant: malformed
     * rows are dropped silently, never a fatal error (matches the rest of this
     * class's philosophy for dangling/invalid references).
     *
     * @param array    $list             Raw token list.
     * @param callable $value_sanitizer  Category-appropriate sanitizer for 'value'/'tabletValue'/'mobileValue'.
     * @return array
     */
    private static function sanitize_token_list( array $list, callable $value_sanitizer ): array {
        $clean = [];
        foreach ( $list as $t ) {
            if ( ! is_array( $t ) ) continue;
            $id = sanitize_key( $t['id'] ?? '' );
            if ( ! $id ) continue;
            $val = $value_sanitizer( (string) ( $t['value'] ?? '' ) );
            if ( '' === $val ) continue;
            $token = [ 'id' => $id, 'name' => sanitize_text_field( $t['name'] ?? '' ), 'value' => $val ];
            foreach ( [ 'tabletValue', 'mobileValue' ] as $rk ) {
                if ( ! empty( $t[ $rk ] ) ) {
                    $rv = $value_sanitizer( (string) $t[ $rk ] );
                    if ( '' !== $rv ) $token[ $rk ] = $rv;
                }
            }
            $clean[] = $token;
        }
        return $clean;
    }

    /** Pick the right value sanitizer callable for a given token category. */
    private static function token_value_sanitizer( string $category ): callable {
        switch ( $category ) {
            case 'color':
                return [ self::class, 'sanitize_color' ];
            case 'boxShadow':
                return [ self::class, 'sanitize_css_value' ];
            case 'fontFamily':
                return 'sanitize_text_field'; // matches existing typography/button preset precedent
            default:
                // fontSize, lineHeight, letterSpacing, borderRadius, spacing, containerWidth
                return [ self::class, 'sanitize_dimension' ];
        }
    }

    // -------------------------------------------------------------------------
    // Reusable class definition sanitizer (Class-Based Styling System)
    // -------------------------------------------------------------------------

    /**
     * Sanitize a reusable class definition before it is persisted to the
     * jalpi_classes table. A class definition is structurally a headless
     * node — styles/tabletStyles/mobileStyles + a background/border settings
     * bucket — so it reuses the exact same sanitizers as node styles.
     *
     * @param  array $data Raw class payload from the REST API.
     * @return array       Sanitized class definition (without id/key — the
     *                      caller is responsible for those).
     */
    public static function sanitize_class_definition( array $data ): array {
        $clean = [
            'name' => sanitize_html_class( (string) ( $data['name'] ?? '' ) ),
        ];

        if ( isset( $data['styles'] ) && is_array( $data['styles'] ) ) {
            $clean['styles'] = self::sanitize_styles( $data['styles'] );
        }

        foreach ( [ 'tabletStyles', 'mobileStyles' ] as $resp_key ) {
            if ( isset( $data[ $resp_key ] ) && is_array( $data[ $resp_key ] ) ) {
                $clean[ $resp_key ] = self::sanitize_styles( $data[ $resp_key ] );
            }
        }

        if ( isset( $data['settings'] ) && is_array( $data['settings'] ) ) {
            $settings       = $data['settings'];
            $clean_settings = [];
            foreach ( [ 'bgNormal', 'bgHover', 'overlayNormal', 'overlayHover' ] as $bg_key ) {
                if ( isset( $settings[ $bg_key ] ) ) {
                    $clean_settings[ $bg_key ] = self::sanitize_bg_data( (array) $settings[ $bg_key ] );
                }
            }
            foreach ( [ 'borderNormal', 'borderHover' ] as $bd_key ) {
                if ( isset( $settings[ $bd_key ] ) ) {
                    $clean_settings[ $bd_key ] = self::sanitize_border_data( (array) $settings[ $bd_key ] );
                }
            }
            $clean['settings'] = $clean_settings;
        }

        // Design System inheritance — same link shape/resolution as node.globalStyleRef,
        // so a class's unset properties can fall through to site-wide tokens/presets.
        if ( isset( $data['globalStyleRef'] ) && is_array( $data['globalStyleRef'] ) ) {
            $clean['globalStyleRef'] = self::sanitize_global_style_ref_data( $data['globalStyleRef'] );
        }

        $clean['favorite'] = (bool) ( $data['favorite'] ?? false );

        return $clean;
    }

    /**
     * Sanitize per-element raw Custom CSS. Author-trusted input (same
     * capability gate as the rest of the builder — edit_post), so this is
     * an injection-vector strip rather than a full CSS parser, matching the
     * approach WordPress core uses for the Customizer's own Additional CSS.
     */
    public static function sanitize_custom_css( string $css ): string {
        $css = substr( $css, 0, 5000 );
        $css = wp_strip_all_tags( $css );
        $dangerous = [
            '/expression\s*\(/i',
            '/-moz-binding\s*:/i',
            '/javascript\s*:/i',
            '/vbscript\s*:/i',
            '/@import\b/i',
            '/<\/style/i',
            '/<script/i',
        ];
        foreach ( $dangerous as $pattern ) {
            $css = preg_replace( $pattern, '', $css );
        }
        return trim( $css );
    }

    // -------------------------------------------------------------------------
    // Settings sanitizer
    // -------------------------------------------------------------------------

    private static function sanitize_settings( string $type, array $settings ): array {
        $clean = [];

        // ── CSS identity (all node types) ────────────────────────────────────
        if ( isset( $settings['cssId'] ) ) {
            $clean['cssId'] = sanitize_html_class( $settings['cssId'] );
        }
        if ( isset( $settings['cssClass'] ) ) {
            $parts = preg_split( '/\s+/', sanitize_text_field( $settings['cssClass'] ), -1, PREG_SPLIT_NO_EMPTY );
            $clean['cssClass'] = implode( ' ', array_map( 'sanitize_html_class', $parts ) );
        }

        // ── Split Text animation (all node types) ────────────────────────────
        // Opt-in per node via st_enabled. Keys are only written to $clean when
        // present in the incoming settings, so content saved before this feature
        // existed keeps an identical settings array — nothing is back-filled.
        //
        // Enum values are re-validated against the canonical whitelists in the
        // Jalpi_Split_Text trait at render time; anything unrecognised there
        // simply emits no attribute.
        if ( isset( $settings['st_enabled'] ) ) {
            $clean['st_enabled'] = ! empty( $settings['st_enabled'] );
        }
        if ( isset( $settings['st_animation'] ) ) {
            $clean['st_animation'] = sanitize_text_field( $settings['st_animation'] );
        }
        if ( isset( $settings['st_split'] ) ) {
            $by = sanitize_key( $settings['st_split'] );
            $clean['st_split'] = in_array( $by, [ 'chars', 'words', 'lines', 'paragraphs' ], true ) ? $by : 'chars';
        }
        if ( isset( $settings['st_trigger'] ) ) {
            $tr = sanitize_key( $settings['st_trigger'] );
            $clean['st_trigger'] = in_array( $tr, [ 'load', 'scroll' ], true ) ? $tr : 'scroll';
        }
        foreach ( [ 'st_duration', 'st_delay', 'st_stagger' ] as $st_key ) {
            if ( isset( $settings[ $st_key ] ) ) {
                $clean[ $st_key ] = min( 20000, absint( $settings[ $st_key ] ) );
            }
        }
        if ( isset( $settings['st_repeat'] ) ) {
            // -1 is the "infinite" sentinel and must survive absint().
            $rp = (int) $settings['st_repeat'];
            $clean['st_repeat'] = ( -1 === $rp ) ? -1 : max( 1, min( 100, $rp ) );
        }
        if ( isset( $settings['st_ease'] ) ) {
            $clean['st_ease'] = sanitize_text_field( $settings['st_ease'] );
        }
        if ( isset( $settings['st_viewport_pct'] ) ) {
            $clean['st_viewport_pct'] = max( 0, min( 100, absint( $settings['st_viewport_pct'] ) ) );
        }
        if ( isset( $settings['st_offset'] ) ) {
            $clean['st_offset'] = sanitize_text_field( $settings['st_offset'] );
        }
        foreach ( [ 'st_replay', 'st_reverse' ] as $st_bool ) {
            if ( isset( $settings[ $st_bool ] ) ) {
                $clean[ $st_bool ] = ! empty( $settings[ $st_bool ] );
            }
        }
        if ( isset( $settings['st_gradient'] ) && is_array( $settings['st_gradient'] ) ) {
            $clean['st_gradient'] = [
                'from'  => self::sanitize_color( $settings['st_gradient']['from'] ?? '' ),
                'to'    => self::sanitize_color( $settings['st_gradient']['to'] ?? '' ),
                'angle' => max( 0, min( 360, absint( $settings['st_gradient']['angle'] ?? 90 ) ) ),
            ];
        }

        // ── Reusable classes (Class-Based Styling System) — all node types ──
        if ( isset( $settings['cssClasses'] ) && is_array( $settings['cssClasses'] ) ) {
            $keys = array_map( 'sanitize_key', $settings['cssClasses'] );
            $keys = array_values( array_unique( array_filter( $keys ) ) );
            $clean['cssClasses'] = array_slice( $keys, 0, 50 );
        }

        // ── Per-element Custom CSS (uses the `selector` placeholder token) ──
        if ( isset( $settings['customCss'] ) ) {
            $clean['customCss'] = self::sanitize_custom_css( (string) $settings['customCss'] );
        }

        // ── Background / Overlay / Border (shared structured keys) ──────────
        foreach ( [ 'bgNormal', 'bgHover', 'overlayNormal', 'overlayHover' ] as $bg_key ) {
            if ( isset( $settings[ $bg_key ] ) ) {
                $clean[ $bg_key ] = self::sanitize_bg_data( (array) $settings[ $bg_key ] );
            }
        }
        foreach ( [ 'borderNormal', 'borderHover' ] as $bd_key ) {
            if ( isset( $settings[ $bd_key ] ) ) {
                $clean[ $bd_key ] = self::sanitize_border_data( (array) $settings[ $bd_key ] );
            }
        }

        // ── Type-specific fields ─────────────────────────────────────────────
        switch ( $type ) {

            case 'section':
                foreach ( [ 'contentWidth' => [ 'full','boxed' ] ] as $k => $allowed ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = in_array( $settings[ $k ], $allowed, true ) ? $settings[ $k ] : 'full';
                    }
                }
                if ( isset( $settings['boxedWidth'] ) ) {
                    $clean['boxedWidth'] = self::sanitize_dimension( $settings['boxedWidth'] );
                }
                if ( isset( $settings['alignItems'] ) ) {
                    $clean['alignItems'] = self::sanitize_flex_align( $settings['alignItems'] );
                }
                if ( isset( $settings['height'] ) ) {
                    $clean['height'] = self::sanitize_dimension( $settings['height'] );
                }
                // Legacy background/padding (backwards compat)
                if ( isset( $settings['background'] ) ) {
                    $clean['background'] = self::sanitize_bg_data( (array) $settings['background'] );
                }
                if ( isset( $settings['padding'] ) && is_array( $settings['padding'] ) ) {
                    $clean['padding'] = array_map(
                        [ self::class, 'sanitize_dimension' ],
                        array_intersect_key( $settings['padding'], array_flip( [ 'top','right','bottom','left' ] ) )
                    );
                }
                break;

            case 'container':
                if ( isset( $settings['contentWidth'] ) ) {
                    $clean['contentWidth'] = in_array( $settings['contentWidth'], [ 'full','boxed' ], true )
                        ? $settings['contentWidth'] : 'full';
                }
                if ( isset( $settings['boxedWidth'] ) ) {
                    $clean['boxedWidth'] = self::sanitize_dimension( $settings['boxedWidth'] );
                }
                if ( isset( $settings['alignItems'] ) ) {
                    $clean['alignItems'] = self::sanitize_flex_align( $settings['alignItems'] );
                }
                if ( isset( $settings['columnGap'] ) ) {
                    $clean['columnGap'] = self::sanitize_dimension( $settings['columnGap'] );
                }

                // ── Layout mode ──────────────────────────────────────────────
                if ( isset( $settings['layoutMode'] ) ) {
                    $clean['layoutMode'] = in_array( $settings['layoutMode'], [ 'block', 'flex', 'grid' ], true )
                        ? $settings['layoutMode'] : 'block';
                }

                // ── Flex settings ────────────────────────────────────────────
                if ( isset( $settings['flexDirection'] ) ) {
                    $clean['flexDirection'] = in_array( $settings['flexDirection'], [ 'row','column','row-reverse','column-reverse' ], true )
                        ? $settings['flexDirection'] : 'row';
                }
                if ( isset( $settings['flexWrapMode'] ) ) {
                    $clean['flexWrapMode'] = in_array( $settings['flexWrapMode'], [ 'nowrap','wrap','wrap-reverse' ], true )
                        ? $settings['flexWrapMode'] : 'nowrap';
                }
                if ( isset( $settings['flexJustify'] ) ) {
                    $clean['flexJustify'] = in_array( $settings['flexJustify'], [ 'flex-start','center','flex-end','space-between','space-around','space-evenly' ], true )
                        ? $settings['flexJustify'] : 'flex-start';
                }
                if ( isset( $settings['flexAlign'] ) ) {
                    $clean['flexAlign'] = self::sanitize_flex_align( $settings['flexAlign'] );
                }
                if ( isset( $settings['flexGap'] ) ) {
                    $clean['flexGap'] = self::sanitize_dimension( $settings['flexGap'] );
                }

                // ── Grid settings ─────────────────────────────────────────────
                if ( isset( $settings['gridAutoMode'] ) ) {
                    $clean['gridAutoMode'] = in_array( $settings['gridAutoMode'], [ 'none','fill','fit' ], true )
                        ? $settings['gridAutoMode'] : 'none';
                }
                if ( isset( $settings['gridColumns'] ) ) {
                    $clean['gridColumns'] = (string) max( 1, min( 12, (int) $settings['gridColumns'] ) );
                }
                if ( isset( $settings['gridRows'] ) ) {
                    $clean['gridRows'] = $settings['gridRows'] === '' ? '' : (string) max( 1, (int) $settings['gridRows'] );
                }
                if ( isset( $settings['gridGap'] ) ) {
                    $clean['gridGap'] = self::sanitize_dimension( $settings['gridGap'] );
                }
                if ( isset( $settings['gridMinColSize'] ) ) {
                    $clean['gridMinColSize'] = self::sanitize_dimension( $settings['gridMinColSize'] );
                }

                // Legacy layout compat
                foreach ( [ 'columns', 'gap', 'direction', 'justifyContent', 'flexWrap' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = sanitize_text_field( (string) $settings[ $k ] );
                    }
                }
                break;

            case 'column':
                if ( isset( $settings['width'] ) ) {
                    $clean['width'] = self::sanitize_dimension( $settings['width'] );
                }
                if ( isset( $settings['alignSelf'] ) ) {
                    $clean['alignSelf'] = self::sanitize_flex_align( $settings['alignSelf'] );
                }
                // Opt-in flex layout for a column's own children, so trailing
                // elements can be pinned to a shared baseline inside an
                // equal-height card row. Same key names as 'container'.
                if ( isset( $settings['layoutMode'] ) && 'flex' === $settings['layoutMode'] ) {
                    $clean['layoutMode'] = 'flex';
                }
                if ( isset( $settings['flexDirection'] ) ) {
                    $clean['flexDirection'] = in_array( $settings['flexDirection'], [ 'row', 'column', 'row-reverse', 'column-reverse' ], true )
                        ? $settings['flexDirection'] : 'column';
                }
                if ( isset( $settings['flexAlign'] ) ) {
                    $clean['flexAlign'] = self::sanitize_flex_align( $settings['flexAlign'] );
                }
                if ( isset( $settings['flexJustify'] ) && in_array( $settings['flexJustify'], [ 'flex-start', 'flex-end', 'center', 'space-between', 'space-around', 'space-evenly' ], true ) ) {
                    $clean['flexJustify'] = $settings['flexJustify'];
                }
                if ( isset( $settings['flexGap'] ) ) {
                    $clean['flexGap'] = self::sanitize_dimension( $settings['flexGap'] );
                }
                break;

            case 'spacer':
                if ( isset( $settings['height'] ) ) {
                    $clean['height'] = self::sanitize_dimension( $settings['height'] );
                }
                break;

            case 'heading':
            case 'text':
                if ( isset( $settings['elWidth'] ) ) {
                    $clean['elWidth'] = self::sanitize_dimension( $settings['elWidth'] );
                }
                break;

            case 'video': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                $allowed_ar = [ '16:9', '4:3', '1:1', '9:16', '21:9', '3:4' ];
                $allowed_bd = [ 'default', 'none', 'solid', 'dashed', 'dotted', 'double' ];
                if ( isset( $settings['vid_aspect'] ) && in_array( $settings['vid_aspect'], $allowed_ar, true ) ) {
                    $clean['vid_aspect'] = $settings['vid_aspect'];
                }
                if ( isset( $settings['vid_bdType'] ) && in_array( $settings['vid_bdType'], $allowed_bd, true ) ) {
                    $clean['vid_bdType'] = $settings['vid_bdType'];
                }
                if ( isset( $settings['vid_bdColor'] ) ) $clean['vid_bdColor'] = $col( $settings['vid_bdColor'] );
                foreach ( [ 'vid_bdw_top', 'vid_bdw_right', 'vid_bdw_bottom', 'vid_bdw_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }
                foreach ( [ 'vid_br_top', 'vid_br_right', 'vid_br_bottom', 'vid_br_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }
                break;
            }

            case 'maps':
                if ( isset( $settings['height'] ) ) {
                    $clean['height'] = self::sanitize_dimension( $settings['height'] );
                }
                break;

            case 'progress': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                // Color settings
                foreach ( [ 'pb_track_bg', 'pb_title_color', 'pb_pct_color', 'pb_inner_color' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $col( $settings[ $k ] );
                }
                // Dimension settings (track + legacy single font-size)
                foreach ( [ 'pb_track_height', 'pb_track_radius' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }
                // Typography for Title / Pct / Inner
                $allowed_fw  = [ '400', '500', '600', '700', '800', '900' ];
                $allowed_tt  = [ '', 'none', 'uppercase', 'lowercase', 'capitalize' ];
                $allowed_fs  = [ '', 'normal', 'italic', 'oblique' ];
                $allowed_td  = [ '', 'none', 'underline', 'line-through', 'overline' ];
                foreach ( [ 'pb_title', 'pb_pct', 'pb_inner' ] as $pfx ) {
                    foreach ( [ 'fontSize', 'lineHeight', 'letterSpacing', 'wordSpacing' ] as $prop ) {
                        $k = $pfx . '_' . $prop;
                        if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                    }
                    $k = $pfx . '_fontWeight';
                    if ( isset( $settings[ $k ] ) && in_array( (string) $settings[ $k ], $allowed_fw, true ) ) $clean[ $k ] = (string) $settings[ $k ];
                    $k = $pfx . '_fontFamily';
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = sanitize_text_field( (string) $settings[ $k ] );
                    $k = $pfx . '_textTransform';
                    if ( isset( $settings[ $k ] ) && in_array( (string) $settings[ $k ], $allowed_tt, true ) ) $clean[ $k ] = (string) $settings[ $k ];
                    $k = $pfx . '_fontStyle';
                    if ( isset( $settings[ $k ] ) && in_array( (string) $settings[ $k ], $allowed_fs, true ) ) $clean[ $k ] = (string) $settings[ $k ];
                    $k = $pfx . '_textDecoration';
                    if ( isset( $settings[ $k ] ) && in_array( (string) $settings[ $k ], $allowed_td, true ) ) $clean[ $k ] = (string) $settings[ $k ];
                    // Text shadow
                    foreach ( [ 'textShadowH', 'textShadowV', 'textShadowBlur' ] as $prop ) {
                        $k = $pfx . '_' . $prop;
                        if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                    }
                    $k = $pfx . '_textShadowColor';
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $col( $settings[ $k ] );
                }
                break;
            }

            case 'counter': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                // Per-state background
                foreach ( [ 'cnt_bg_normal', 'cnt_bg_hover' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $col( $settings[ $k ] );
                }
                // Border
                $allowed_bd = [ 'default', 'none', 'solid', 'dashed', 'dotted', 'double', 'groove' ];
                if ( isset( $settings['cnt_bdType'] ) ) {
                    $clean['cnt_bdType'] = in_array( $settings['cnt_bdType'], $allowed_bd, true ) ? $settings['cnt_bdType'] : 'default';
                }
                if ( isset( $settings['cnt_bdColor'] ) ) $clean['cnt_bdColor'] = $col( $settings['cnt_bdColor'] );
                foreach ( [ 'cnt_bdw_top', 'cnt_bdw_right', 'cnt_bdw_bottom', 'cnt_bdw_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }
                foreach ( [ 'cnt_br_top', 'cnt_br_right', 'cnt_br_bottom', 'cnt_br_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }
                foreach ( [ 'cnt_pad_top', 'cnt_pad_right', 'cnt_pad_bottom', 'cnt_pad_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }
                // Item spacing (gap between counter items)
                if ( isset( $settings['cnt_gap'] ) ) $clean['cnt_gap'] = $dim( $settings['cnt_gap'] );
                // Item layout controls
                $allowed_title_pos   = [ 'top', 'bottom', 'left', 'right' ];
                $allowed_title_align = [ 'left', 'center', 'right' ];
                $allowed_num_pos     = [ 'left', 'center', 'right', 'justify' ];
                if ( isset( $settings['cnt_titlePos'] )   && in_array( $settings['cnt_titlePos'],   $allowed_title_pos,   true ) ) $clean['cnt_titlePos']   = $settings['cnt_titlePos'];
                if ( isset( $settings['cnt_titleAlign'] ) && in_array( $settings['cnt_titleAlign'], $allowed_title_align, true ) ) $clean['cnt_titleAlign'] = $settings['cnt_titleAlign'];
                if ( isset( $settings['cnt_titleGap'] ) )  $clean['cnt_titleGap'] = $dim( $settings['cnt_titleGap'] );
                if ( isset( $settings['cnt_numPos'] )     && in_array( $settings['cnt_numPos'],     $allowed_num_pos,     true ) ) $clean['cnt_numPos']     = $settings['cnt_numPos'];
                // Number styles
                foreach ( [ 'cnt_num_color', 'cnt_title_color' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $col( $settings[ $k ] );
                }
                foreach ( [ 'cnt_num_fontSize', 'cnt_title_fontSize' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }
                $allowed_fw = [ '400', '500', '600', '700', '800', '900' ];
                foreach ( [ 'cnt_num_fontWeight', 'cnt_title_fontWeight' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && in_array( (string) $settings[ $k ], $allowed_fw, true ) ) {
                        $clean[ $k ] = (string) $settings[ $k ];
                    }
                }
                break;
            }

            case 'accordion': {
                // Helper closures
                $dim   = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col   = static fn( $v ) => self::sanitize_color( (string) $v );
                $key   = static fn( $v ) => sanitize_key( (string) $v );

                // Global spacing
                foreach ( [ 'acc_item_gap', 'acc_content_gap' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }

                // Per-state background (normal / hover / active)
                foreach ( [ 'acc_bg_normal', 'acc_bg_hover', 'acc_bg_active' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $col( $settings[ $k ] );
                }

                // Global border
                $allowed_bd = [ 'default', 'none', 'solid', 'dashed', 'dotted', 'double', 'groove' ];
                if ( isset( $settings['acc_bdType'] ) ) {
                    $clean['acc_bdType'] = in_array( $settings['acc_bdType'], $allowed_bd, true ) ? $settings['acc_bdType'] : 'default';
                }
                if ( isset( $settings['acc_bdColor'] ) ) $clean['acc_bdColor'] = $col( $settings['acc_bdColor'] );
                foreach ( [ 'acc_bdw_top', 'acc_bdw_right', 'acc_bdw_bottom', 'acc_bdw_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }

                // Global border radius
                foreach ( [ 'acc_br_top', 'acc_br_right', 'acc_br_bottom', 'acc_br_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }

                // Global padding
                foreach ( [ 'acc_pad_top', 'acc_pad_right', 'acc_pad_bottom', 'acc_pad_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }

                // Header settings
                foreach ( [ 'acc_hdr_bg', 'acc_hdr_color' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $col( $settings[ $k ] );
                }
                if ( isset( $settings['acc_hdr_fontSize'] ) ) $clean['acc_hdr_fontSize'] = $dim( $settings['acc_hdr_fontSize'] );
                foreach ( [ 'acc_hdr_pad_top', 'acc_hdr_pad_right', 'acc_hdr_pad_bottom', 'acc_hdr_pad_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }

                // Content body settings
                foreach ( [ 'acc_body_bg', 'acc_body_color' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $col( $settings[ $k ] );
                }
                if ( isset( $settings['acc_body_fontSize'] ) ) $clean['acc_body_fontSize'] = $dim( $settings['acc_body_fontSize'] );
                foreach ( [ 'acc_body_pad_top', 'acc_body_pad_right', 'acc_body_pad_bottom', 'acc_body_pad_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) $clean[ $k ] = $dim( $settings[ $k ] );
                }
                break;
            }

            case 'image': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                $pct = static fn( $v ) => ( is_numeric( $v ) && (float) $v >= 0 && (float) $v <= 200 ) ? (string) (float) $v : '';

                $allowed_fit = [ 'fill', 'cover', 'contain', 'none', 'scale-down' ];
                $allowed_bd  = [ 'default', 'none', 'solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'inset', 'outset' ];

                if ( isset( $settings['img_objectFit'] ) && in_array( $settings['img_objectFit'], $allowed_fit, true ) )
                    $clean['img_objectFit'] = $settings['img_objectFit'];

                foreach ( [ 'img_opacity', 'img_opacityHover' ] as $k ) {
                    if ( isset( $settings[$k] ) && is_numeric( $settings[$k] ) ) {
                        $v = (float) $settings[$k];
                        if ( $v >= 0 && $v <= 100 ) $clean[$k] = (string) $v;
                    }
                }

                foreach ( [ 'img_blur', 'img_blurHover' ] as $k ) {
                    if ( isset( $settings[$k] ) ) $clean[$k] = $dim( $settings[$k] );
                }
                foreach ( [ 'img_brightness', 'img_brightnessHover', 'img_contrast', 'img_contrastHover',
                             'img_saturate',   'img_saturateHover' ] as $k ) {
                    if ( isset( $settings[$k] ) ) $clean[$k] = $pct( $settings[$k] );
                }
                foreach ( [ 'img_hue', 'img_hueHover' ] as $k ) {
                    if ( isset( $settings[$k] ) && is_numeric( $settings[$k] ) ) {
                        $v = (int) $settings[$k];
                        if ( $v >= 0 && $v <= 360 ) $clean[$k] = (string) $v;
                    }
                }

                if ( isset( $settings['img_bdType'] ) && in_array( $settings['img_bdType'], $allowed_bd, true ) )
                    $clean['img_bdType'] = $settings['img_bdType'];
                foreach ( [ 'img_bdw_top', 'img_bdw_right', 'img_bdw_bottom', 'img_bdw_left' ] as $k ) {
                    if ( isset( $settings[$k] ) ) $clean[$k] = $dim( $settings[$k] );
                }
                if ( isset( $settings['img_bdColor'] ) ) $clean['img_bdColor'] = $col( $settings['img_bdColor'] );

                foreach ( [ 'img_br_top', 'img_br_right', 'img_br_bottom', 'img_br_left' ] as $k ) {
                    if ( isset( $settings[$k] ) ) $clean[$k] = $dim( $settings[$k] );
                }

                if ( isset( $settings['img_boxShadow'] ) )
                    $clean['img_boxShadow'] = sanitize_text_field( $settings['img_boxShadow'] );

                // Caption styles
                $allowed_cap_align = [ 'left', 'center', 'right', 'justify' ];
                if ( isset( $settings['img_cap_textAlign'] ) && in_array( $settings['img_cap_textAlign'], $allowed_cap_align, true ) )
                    $clean['img_cap_textAlign'] = $settings['img_cap_textAlign'];
                foreach ( [ 'img_cap_color', 'img_cap_bgColor', 'img_cap_textShadowColor' ] as $k ) {
                    if ( isset( $settings[$k] ) ) $clean[$k] = $col( $settings[$k] );
                }
                if ( isset( $settings['img_cap_spacing'] ) ) $clean['img_cap_spacing'] = $dim( $settings['img_cap_spacing'] );
                foreach ( [ 'img_cap_textShadowH', 'img_cap_textShadowV', 'img_cap_textShadowBlur' ] as $k ) {
                    if ( isset( $settings[$k] ) ) $clean[$k] = $dim( $settings[$k] );
                }
                foreach ( [ 'img_cap_fontFamily', 'img_cap_fontSize', 'img_cap_fontWeight',
                             'img_cap_lineHeight', 'img_cap_letterSpacing', 'img_cap_textTransform' ] as $k ) {
                    if ( isset( $settings[$k] ) ) $clean[$k] = sanitize_text_field( $settings[$k] );
                }

                break;
            }

            case 'breadcrumbs': {
                foreach ( [ 'bc_link_color', 'bc_link_hover', 'bc_current_color', 'bc_sep_color' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = self::sanitize_color( (string) $settings[ $k ] );
                    }
                }
                foreach ( [ 'bc_gap', 'bc_font_size' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = self::sanitize_dimension( (string) $settings[ $k ] );
                    }
                }
                break;
            }

            case 'menu': {
                // Style-tab keys. The element re-validates each value's shape
                // before it reaches CSS, so anything malformed simply renders
                // no declaration rather than a broken rule.
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );

                foreach ( [ 'menu_color', 'menu_color_hover', 'menu_color_active',
                            'menu_bg', 'menu_bg_hover', 'menu_bg_active',
                            'menu_border_color', 'submenu_bg', 'submenu_color' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = $col( $settings[ $k ] );
                    }
                }

                foreach ( [ 'menu_font_size', 'menu_letter_spacing', 'menu_gap',
                            'menu_border_width', 'submenu_width' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = $dim( $settings[ $k ] );
                    }
                }

                // 1-4 value shorthands. The element already renders these via
                // len4(); previously sanitize_dimension() discarded anything
                // with a space, so a value like "10px 14px" never survived the
                // save. Stored values are unaffected — sanitizing happens only
                // on save, so existing pages already hold '' here and their
                // output does not change.
                foreach ( [ 'menu_padding', 'menu_margin', 'menu_radius',
                            'submenu_radius', 'submenu_padding' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = self::sanitize_dimension_shorthand( (string) $settings[ $k ] );
                    }
                }

                if ( isset( $settings['menu_font_weight'] ) ) {
                    $clean['menu_font_weight'] = sanitize_text_field( $settings['menu_font_weight'] );
                }
                if ( isset( $settings['menu_text_transform'] ) ) {
                    $tt = sanitize_key( $settings['menu_text_transform'] );
                    $clean['menu_text_transform'] = in_array( $tt, [ 'none', 'uppercase', 'lowercase', 'capitalize' ], true )
                        ? $tt : 'none';
                }
                if ( isset( $settings['menu_border_style'] ) ) {
                    $bs = sanitize_key( $settings['menu_border_style'] );
                    $clean['menu_border_style'] = in_array( $bs, [ 'none', 'solid', 'dashed', 'dotted', 'double' ], true )
                        ? $bs : 'none';
                }
                // Box shadow is stored as parts and reassembled by the element,
                // so no raw CSS string is ever accepted.
                if ( isset( $settings['menu_shadow'] ) && is_array( $settings['menu_shadow'] ) ) {
                    $sh = $settings['menu_shadow'];
                    $clean['menu_shadow'] = [
                        'x'     => (int) ( $sh['x'] ?? 0 ),
                        'y'     => (int) ( $sh['y'] ?? 0 ),
                        'blur'  => max( 0, (int) ( $sh['blur'] ?? 0 ) ),
                        'color' => $col( $sh['color'] ?? '' ),
                    ];
                }
                break;
            }

            case 'adv-menu': {
                // Style-tab keys. The element re-validates each value's shape
                // before it reaches CSS, so anything malformed simply renders
                // no declaration rather than a broken rule.
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );

                // Padding / margin / radius are free-text 1-4 value fields, so
                // they need the shorthand-aware pass — plain sanitize_dimension()
                // would discard anything containing a space.
                $dim4 = static fn( $v ) => self::sanitize_dimension_shorthand( (string) $v );

                foreach ( [
                    'am_color', 'am_color_hover', 'am_color_active',
                    'am_bg', 'am_bg_hover', 'am_bg_active',
                    'am_border_color',
                    'am_panel_bg', 'am_panel_overlay', 'am_panel_border_color',
                    'am_panel_grad_color1', 'am_panel_grad_color2',
                    'am_col_divider_color',
                    'am_heading_color',
                    'am_plink_color', 'am_plink_color_hover',
                    'am_icon_color', 'am_icon_color_hover',
                    'am_btn_color', 'am_btn_color_hover', 'am_btn_bg', 'am_btn_bg_hover',
                    'am_btn_border_color',
                    'am_sub_bg', 'am_sub_color',
                    'am_burger_color', 'am_burger_color_hover',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = $col( $settings[ $k ] );
                    }
                }

                // Single-value keys.
                foreach ( [
                    'am_font_size', 'am_line_height', 'am_letter_spacing',
                    'am_gap', 'am_border_width',
                    'am_panel_border_width',
                    'am_col_gap', 'am_col_divider_width',
                    'am_heading_font_size',
                    'am_plink_font_size',
                    'am_icon_size', 'am_icon_spacing',
                    'am_btn_font_size', 'am_btn_border_width',
                    'am_sub_width',
                    'am_burger_size',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = $dim( $settings[ $k ] );
                    }
                }

                // 1-4 value shorthand keys (padding / margin / radius).
                foreach ( [
                    'am_padding', 'am_margin', 'am_radius',
                    'am_panel_padding', 'am_panel_margin', 'am_panel_radius',
                    'am_heading_margin',
                    'am_img_radius',
                    'am_btn_radius',
                    'am_sub_radius', 'am_sub_padding',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = $dim4( $settings[ $k ] );
                    }
                }

                // Font families are free text; the element restricts them to
                // family-list characters before they reach CSS.
                foreach ( [ 'am_font_family', 'am_heading_font_family', 'am_plink_font_family', 'am_btn_font_family' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = sanitize_text_field( $settings[ $k ] );
                    }
                }

                foreach ( [ 'am_font_weight', 'am_heading_font_weight', 'am_plink_font_weight', 'am_btn_font_weight' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = sanitize_text_field( $settings[ $k ] );
                    }
                }

                if ( isset( $settings['am_text_transform'] ) ) {
                    $tt = sanitize_key( $settings['am_text_transform'] );
                    $clean['am_text_transform'] = in_array( $tt, [ 'none', 'uppercase', 'lowercase', 'capitalize' ], true )
                        ? $tt : 'none';
                }

                $allowed_bs = [ 'none', 'solid', 'dashed', 'dotted', 'double' ];
                foreach ( [ 'am_border_style', 'am_panel_border_style', 'am_btn_border_style' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $bs          = sanitize_key( $settings[ $k ] );
                        $clean[ $k ] = in_array( $bs, $allowed_bs, true ) ? $bs : 'none';
                    }
                }

                if ( isset( $settings['am_panel_bg_type'] ) ) {
                    $bt = sanitize_key( $settings['am_panel_bg_type'] );
                    $clean['am_panel_bg_type'] = in_array( $bt, [ 'classic', 'gradient' ], true ) ? $bt : 'classic';
                }
                if ( isset( $settings['am_panel_grad_angle'] ) ) {
                    $clean['am_panel_grad_angle'] = sanitize_text_field( $settings['am_panel_grad_angle'] );
                }

                if ( isset( $settings['am_col_align'] ) ) {
                    $ca = sanitize_key( $settings['am_col_align'] );
                    $clean['am_col_align'] = in_array( $ca, [ 'left', 'center', 'right' ], true ) ? $ca : 'left';
                }
                if ( isset( $settings['am_img_hover'] ) ) {
                    $ih = sanitize_key( $settings['am_img_hover'] );
                    $clean['am_img_hover'] = in_array( $ih, [ 'none', 'zoom', 'lift', 'fade' ], true ) ? $ih : 'none';
                }

                // Box shadows are stored as parts and reassembled by the
                // element, so no raw CSS string is ever accepted.
                foreach ( [ 'am_shadow', 'am_panel_shadow', 'am_img_shadow' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && is_array( $settings[ $k ] ) ) {
                        $sh          = $settings[ $k ];
                        $clean[ $k ] = [
                            'x'      => (int) ( $sh['x'] ?? 0 ),
                            'y'      => (int) ( $sh['y'] ?? 0 ),
                            'blur'   => max( 0, (int) ( $sh['blur'] ?? 0 ) ),
                            'spread' => (int) ( $sh['spread'] ?? 0 ),
                            'color'  => $col( $sh['color'] ?? '' ),
                        ];
                    }
                }
                break;
            }

            case 'tabs-adv': {
                // Style-tab keys for the tab buttons. Layout gap/distance plus
                // per-state (normal/hover/active) bg/color/border/shadow/4-side
                // radius+padding. All sanitized; unknown keys dropped.
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                $allowed_bt = [ 'none', 'solid', 'dashed', 'dotted', 'double' ];

                foreach ( [ 'tab_gap', 'tab_distance' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) { $clean[ $k ] = $dim( $settings[ $k ] ); }
                }
                foreach ( [ 'normal', 'hover', 'active' ] as $state ) {
                    $p = 'tab_' . $state . '_';
                    if ( isset( $settings[ $p . 'bg' ] ) )         { $clean[ $p . 'bg' ]    = $col( $settings[ $p . 'bg' ] ); }
                    if ( isset( $settings[ $p . 'color' ] ) )      { $clean[ $p . 'color' ] = $col( $settings[ $p . 'color' ] ); }
                    if ( isset( $settings[ $p . 'borderType' ] ) && in_array( $settings[ $p . 'borderType' ], $allowed_bt, true ) ) {
                        $clean[ $p . 'borderType' ] = $settings[ $p . 'borderType' ];
                    }
                    if ( isset( $settings[ $p . 'borderColor' ] ) ) { $clean[ $p . 'borderColor' ] = $col( $settings[ $p . 'borderColor' ] ); }
                    if ( isset( $settings[ $p . 'boxShadow' ] ) )   { $clean[ $p . 'boxShadow' ]   = self::sanitize_css_value( (string) $settings[ $p . 'boxShadow' ] ); }
                    foreach ( [ 'borderWidth', 'radius', 'padding' ] as $box ) {
                        foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
                            $key = $p . $box . '_' . $side;
                            if ( isset( $settings[ $key ] ) ) { $clean[ $key ] = $dim( $settings[ $key ] ); }
                        }
                    }
                }

                // ── Titles: state-independent typography + per-state color /
                //    text-shadow / text-stroke on the tab-button title. ──
                $allowed_fs = [ 'normal', 'italic', 'oblique' ];
                $allowed_tt = [ 'none', 'uppercase', 'lowercase', 'capitalize' ];
                $allowed_td = [ 'none', 'underline', 'overline', 'line-through' ];
                if ( isset( $settings['title_fontFamily'] ) )    { $clean['title_fontFamily']    = sanitize_text_field( $settings['title_fontFamily'] ); }
                if ( isset( $settings['title_fontSize'] ) )      { $clean['title_fontSize']      = $dim( $settings['title_fontSize'] ); }
                if ( isset( $settings['title_fontWeight'] ) && preg_match( '/^(100|200|300|400|500|600|700|800|900|normal|bold)$/', (string) $settings['title_fontWeight'] ) ) {
                    $clean['title_fontWeight'] = (string) $settings['title_fontWeight'];
                }
                if ( isset( $settings['title_lineHeight'] ) )    { $clean['title_lineHeight']    = $dim( $settings['title_lineHeight'] ); }
                if ( isset( $settings['title_letterSpacing'] ) ) { $clean['title_letterSpacing'] = $dim( $settings['title_letterSpacing'] ); }
                if ( isset( $settings['title_textTransform'] ) && in_array( $settings['title_textTransform'], $allowed_tt, true ) ) { $clean['title_textTransform'] = $settings['title_textTransform']; }
                if ( isset( $settings['title_fontStyle'] ) && in_array( $settings['title_fontStyle'], $allowed_fs, true ) )         { $clean['title_fontStyle'] = $settings['title_fontStyle']; }
                if ( isset( $settings['title_textDecoration'] ) && in_array( $settings['title_textDecoration'], $allowed_td, true ) ) { $clean['title_textDecoration'] = $settings['title_textDecoration']; }
                foreach ( [ 'normal', 'hover', 'active' ] as $state ) {
                    $tp = 'title_' . $state . '_';
                    if ( isset( $settings[ $tp . 'color' ] ) )       { $clean[ $tp . 'color' ]       = $col( $settings[ $tp . 'color' ] ); }
                    if ( isset( $settings[ $tp . 'strokeWidth' ] ) ) { $clean[ $tp . 'strokeWidth' ] = $dim( $settings[ $tp . 'strokeWidth' ] ); }
                    if ( isset( $settings[ $tp . 'strokeColor' ] ) ) { $clean[ $tp . 'strokeColor' ] = $col( $settings[ $tp . 'strokeColor' ] ); }
                    foreach ( [ 'textShadowH', 'textShadowV', 'textShadowBlur' ] as $sk ) {
                        if ( isset( $settings[ $tp . $sk ] ) ) { $clean[ $tp . $sk ] = $dim( $settings[ $tp . $sk ] ); }
                    }
                    if ( isset( $settings[ $tp . 'textShadowColor' ] ) ) { $clean[ $tp . 'textShadowColor' ] = $col( $settings[ $tp . 'textShadowColor' ] ); }
                }

                // ── Icon: position / size / spacing + per-state color ──
                if ( isset( $settings['icon_position'] ) && in_array( $settings['icon_position'], [ 'top', 'right', 'bottom', 'left' ], true ) ) {
                    $clean['icon_position'] = $settings['icon_position'];
                }
                if ( isset( $settings['icon_size'] ) )    { $clean['icon_size']    = $dim( $settings['icon_size'] ); }
                if ( isset( $settings['icon_spacing'] ) ) { $clean['icon_spacing'] = $dim( $settings['icon_spacing'] ); }
                foreach ( [ 'normal', 'hover', 'active' ] as $state ) {
                    $ik = 'icon_' . $state . '_color';
                    if ( isset( $settings[ $ik ] ) ) { $clean[ $ik ] = $col( $settings[ $ik ] ); }
                }

                // ── Content area: background / border / radius / padding ──
                if ( isset( $settings['content_bgType'] ) && in_array( $settings['content_bgType'], [ 'classic', 'gradient' ], true ) ) {
                    $clean['content_bgType'] = $settings['content_bgType'];
                }
                foreach ( [ 'content_bg', 'content_gradColor1', 'content_gradColor2', 'content_borderColor' ] as $ck ) {
                    if ( isset( $settings[ $ck ] ) ) { $clean[ $ck ] = $col( $settings[ $ck ] ); }
                }
                if ( isset( $settings['content_gradAngle'] ) ) { $clean['content_gradAngle'] = $dim( $settings['content_gradAngle'] ); }
                if ( isset( $settings['content_borderType'] ) && in_array( $settings['content_borderType'], $allowed_bt, true ) ) {
                    $clean['content_borderType'] = $settings['content_borderType'];
                }
                foreach ( [ 'borderWidth', 'radius', 'padding' ] as $box ) {
                    foreach ( [ 'top', 'right', 'bottom', 'left' ] as $side ) {
                        $key = 'content_' . $box . '_' . $side;
                        if ( isset( $settings[ $key ] ) ) { $clean[ $key ] = $dim( $settings[ $key ] ); }
                    }
                }
                break;
            }

            case 'faq': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                $fw  = static fn( $v ) => in_array( (string)$v, ['100','200','300','400','500','600','700','800','900'], true ) ? (string)$v : '';
                $allowed_layouts  = [ 'classic', 'boxed', 'minimal', 'separated', 'framed' ];
                $allowed_icons    = [ 'plus', 'chevron', 'arrow', 'circle', 'none' ];
                $allowed_icon_pos = [ 'right', 'left' ];
                $allowed_bd       = [ 'none', 'solid', 'dashed', 'dotted' ];
                $allowed_ease     = [ 'ease', 'ease-in-out', 'ease-in', 'ease-out', 'linear' ];
                $allowed_fs       = [ 'normal', 'italic', 'oblique' ];
                $allowed_tt       = [ 'none', 'uppercase', 'lowercase', 'capitalize' ];
                $allowed_td       = [ 'none', 'underline', 'overline', 'line-through' ];

                if ( isset($settings['faq_layout'])   && in_array($settings['faq_layout'],   $allowed_layouts,  true) ) $clean['faq_layout']   = $settings['faq_layout'];
                if ( isset($settings['faq_item_gap']) ) $clean['faq_item_gap'] = $dim( $settings['faq_item_gap'] );
                if ( isset($settings['faq_multiple_open']) ) $clean['faq_multiple_open'] = $settings['faq_multiple_open'] ? '1' : '0';
                if ( isset($settings['faq_icon'])     && in_array($settings['faq_icon'],     $allowed_icons,    true) ) $clean['faq_icon']     = $settings['faq_icon'];
                if ( isset($settings['faq_icon_pos']) && in_array($settings['faq_icon_pos'], $allowed_icon_pos, true) ) $clean['faq_icon_pos'] = $settings['faq_icon_pos'];
                if ( isset($settings['faq_icon_size'])        ) $clean['faq_icon_size']         = $dim( $settings['faq_icon_size'] );
                if ( isset($settings['faq_icon_color'])       ) $clean['faq_icon_color']        = $col( $settings['faq_icon_color'] );
                if ( isset($settings['faq_icon_active_color']) ) $clean['faq_icon_active_color'] = $col( $settings['faq_icon_active_color'] );
                if ( isset($settings['faq_anim_dur'])  ) $clean['faq_anim_dur']  = $dim( $settings['faq_anim_dur'] );
                if ( isset($settings['faq_anim_ease']) && in_array($settings['faq_anim_ease'], $allowed_ease, true) ) $clean['faq_anim_ease'] = $settings['faq_anim_ease'];

                // Question typography
                if ( isset($settings['faq_q_color'])        ) $clean['faq_q_color']        = $col( $settings['faq_q_color'] );
                if ( isset($settings['faq_q_active_color']) ) $clean['faq_q_active_color']  = $col( $settings['faq_q_active_color'] );
                if ( isset($settings['faq_q_hover_color'])  ) $clean['faq_q_hover_color']   = $col( $settings['faq_q_hover_color'] );
                if ( isset($settings['faq_q_fontFamily'])   ) $clean['faq_q_fontFamily']    = sanitize_text_field( $settings['faq_q_fontFamily'] );
                if ( isset($settings['faq_q_fontSize'])     ) $clean['faq_q_fontSize']      = $dim( $settings['faq_q_fontSize'] );
                $clean['faq_q_fontWeight'] = $fw( $settings['faq_q_fontWeight'] ?? '' );
                if ( isset($settings['faq_q_fontStyle'])      && in_array($settings['faq_q_fontStyle'],      $allowed_fs, true) ) $clean['faq_q_fontStyle']      = $settings['faq_q_fontStyle'];
                if ( isset($settings['faq_q_textTransform'])  && in_array($settings['faq_q_textTransform'],  $allowed_tt, true) ) $clean['faq_q_textTransform']  = $settings['faq_q_textTransform'];
                if ( isset($settings['faq_q_textDecoration']) && in_array($settings['faq_q_textDecoration'], $allowed_td, true) ) $clean['faq_q_textDecoration'] = $settings['faq_q_textDecoration'];
                if ( isset($settings['faq_q_lineHeight'])    ) $clean['faq_q_lineHeight']    = $dim( $settings['faq_q_lineHeight'] );
                if ( isset($settings['faq_q_letterSpacing']) ) $clean['faq_q_letterSpacing'] = $dim( $settings['faq_q_letterSpacing'] );
                if ( isset($settings['faq_q_pad_v']) ) $clean['faq_q_pad_v'] = $dim( $settings['faq_q_pad_v'] );
                if ( isset($settings['faq_q_pad_h']) ) $clean['faq_q_pad_h'] = $dim( $settings['faq_q_pad_h'] );

                // Answer typography
                if ( isset($settings['faq_a_color'])        ) $clean['faq_a_color']        = $col( $settings['faq_a_color'] );
                if ( isset($settings['faq_a_fontFamily'])   ) $clean['faq_a_fontFamily']   = sanitize_text_field( $settings['faq_a_fontFamily'] );
                if ( isset($settings['faq_a_fontSize'])     ) $clean['faq_a_fontSize']     = $dim( $settings['faq_a_fontSize'] );
                $clean['faq_a_fontWeight'] = $fw( $settings['faq_a_fontWeight'] ?? '' );
                if ( isset($settings['faq_a_fontStyle'])      && in_array($settings['faq_a_fontStyle'],      $allowed_fs, true) ) $clean['faq_a_fontStyle']      = $settings['faq_a_fontStyle'];
                if ( isset($settings['faq_a_textTransform'])  && in_array($settings['faq_a_textTransform'],  $allowed_tt, true) ) $clean['faq_a_textTransform']  = $settings['faq_a_textTransform'];
                if ( isset($settings['faq_a_textDecoration']) && in_array($settings['faq_a_textDecoration'], $allowed_td, true) ) $clean['faq_a_textDecoration'] = $settings['faq_a_textDecoration'];
                if ( isset($settings['faq_a_lineHeight'])    ) $clean['faq_a_lineHeight']    = $dim( $settings['faq_a_lineHeight'] );
                if ( isset($settings['faq_a_letterSpacing']) ) $clean['faq_a_letterSpacing'] = $dim( $settings['faq_a_letterSpacing'] );
                if ( isset($settings['faq_a_pad_v']) ) $clean['faq_a_pad_v'] = $dim( $settings['faq_a_pad_v'] );
                if ( isset($settings['faq_a_pad_h']) ) $clean['faq_a_pad_h'] = $dim( $settings['faq_a_pad_h'] );

                // Item styling
                if ( isset($settings['faq_item_bg'])           ) $clean['faq_item_bg']            = $col( $settings['faq_item_bg'] );
                if ( isset($settings['faq_item_active_bg'])    ) $clean['faq_item_active_bg']     = $col( $settings['faq_item_active_bg'] );
                if ( isset($settings['faq_item_hover_bg'])     ) $clean['faq_item_hover_bg']      = $col( $settings['faq_item_hover_bg'] );
                if ( isset($settings['faq_item_bd_type'])      && in_array($settings['faq_item_bd_type'], $allowed_bd, true) ) $clean['faq_item_bd_type'] = $settings['faq_item_bd_type'];
                if ( isset($settings['faq_item_bd_color'])     ) $clean['faq_item_bd_color']      = $col( $settings['faq_item_bd_color'] );
                if ( isset($settings['faq_item_active_bd_color']) ) $clean['faq_item_active_bd_color'] = $col( $settings['faq_item_active_bd_color'] );
                if ( isset($settings['faq_item_bd_width'])     ) $clean['faq_item_bd_width']      = $dim( $settings['faq_item_bd_width'] );
                if ( isset($settings['faq_item_br'])           ) $clean['faq_item_br']            = $dim( $settings['faq_item_br'] );
                if ( isset($settings['faq_item_pad_v'])        ) $clean['faq_item_pad_v']         = $dim( $settings['faq_item_pad_v'] );
                if ( isset($settings['faq_item_pad_h'])        ) $clean['faq_item_pad_h']         = $dim( $settings['faq_item_pad_h'] );
                if ( isset($settings['faq_item_shadow'])       ) $clean['faq_item_shadow']        = sanitize_text_field( $settings['faq_item_shadow'] );
                if ( isset($settings['faq_item_active_shadow']) ) $clean['faq_item_active_shadow'] = sanitize_text_field( $settings['faq_item_active_shadow'] );
                if ( isset($settings['faq_item_hover_shadow']) ) $clean['faq_item_hover_shadow']  = sanitize_text_field( $settings['faq_item_hover_shadow'] );
                break;
            }

            case 'image-carousel': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                $allowed_layouts = [ 'classic', 'card', 'coverflow', 'modern', 'masonry' ];
                $allowed_effects = [ 'slide', 'fade' ];
                $allowed_fits    = [ 'cover', 'contain', 'fill' ];
                $allowed_hovers  = [ 'none', 'zoom', 'scale', 'fade', 'blur', 'overlay' ];
                $allowed_caps    = [ 'none', 'below', 'overlay' ];
                // Layout & Behaviour
                if ( isset($settings['ic_layout'])   && in_array($settings['ic_layout'], $allowed_layouts, true) ) $clean['ic_layout'] = $settings['ic_layout'];
                if ( isset($settings['ic_cols'])      ) $clean['ic_cols']        = max( 1, min( 6, intval($settings['ic_cols']) ) );
                if ( isset($settings['ic_cols_tablet']) ) $clean['ic_cols_tablet'] = max( 1, min( 4, intval($settings['ic_cols_tablet']) ) );
                if ( isset($settings['ic_cols_mobile']) ) $clean['ic_cols_mobile'] = max( 1, min( 2, intval($settings['ic_cols_mobile']) ) );
                if ( isset($settings['ic_gap'])       ) $clean['ic_gap']         = max( 0, intval($settings['ic_gap']) );
                if ( isset($settings['ic_effect'])    && in_array($settings['ic_effect'], $allowed_effects, true) ) $clean['ic_effect'] = $settings['ic_effect'];
                if ( isset($settings['ic_speed'])     ) $clean['ic_speed']       = max( 100, min( 3000, intval($settings['ic_speed']) ) );
                if ( isset($settings['ic_autoplay'])  ) $clean['ic_autoplay']    = max( 0, intval($settings['ic_autoplay']) );
                if ( isset($settings['ic_loop'])      ) $clean['ic_loop']        = $settings['ic_loop'] ? '1' : '0';
                if ( isset($settings['ic_arrows'])    ) $clean['ic_arrows']      = $settings['ic_arrows'] ? '1' : '0';
                if ( isset($settings['ic_dots'])      ) $clean['ic_dots']        = $settings['ic_dots'] ? '1' : '0';
                if ( isset($settings['ic_lightbox'])  ) $clean['ic_lightbox']    = $settings['ic_lightbox'] ? '1' : '0';
                // Image
                if ( isset($settings['ic_img_height']) ) $clean['ic_img_height'] = $dim($settings['ic_img_height']);
                if ( isset($settings['ic_img_fit'])    && in_array($settings['ic_img_fit'], $allowed_fits, true) ) $clean['ic_img_fit'] = $settings['ic_img_fit'];
                if ( isset($settings['ic_img_br'])     ) $clean['ic_img_br']     = $dim($settings['ic_img_br']);
                // Hover
                if ( isset($settings['ic_hover'])               && in_array($settings['ic_hover'], $allowed_hovers, true) ) $clean['ic_hover'] = $settings['ic_hover'];
                if ( isset($settings['ic_hover_overlay_color']) ) $clean['ic_hover_overlay_color'] = $col($settings['ic_hover_overlay_color']);
                // Caption
                if ( isset($settings['ic_caption_pos'])  && in_array($settings['ic_caption_pos'], $allowed_caps, true) ) $clean['ic_caption_pos'] = $settings['ic_caption_pos'];
                if ( isset($settings['ic_caption_color']) ) $clean['ic_caption_color'] = $col($settings['ic_caption_color']);
                if ( isset($settings['ic_caption_size'])  ) $clean['ic_caption_size']  = $dim($settings['ic_caption_size']);
                if ( isset($settings['ic_desc_color'])    ) $clean['ic_desc_color']    = $col($settings['ic_desc_color']);
                if ( isset($settings['ic_desc_size'])     ) $clean['ic_desc_size']     = $dim($settings['ic_desc_size']);
                if ( isset($settings['ic_caption_bg'])    ) $clean['ic_caption_bg']    = $col($settings['ic_caption_bg']);
                // Arrows
                if ( isset($settings['ic_arrow_color']) ) $clean['ic_arrow_color'] = $col($settings['ic_arrow_color']);
                if ( isset($settings['ic_arrow_bg'])    ) $clean['ic_arrow_bg']    = $col($settings['ic_arrow_bg']);
                if ( isset($settings['ic_arrow_size'])  ) $clean['ic_arrow_size']  = $dim($settings['ic_arrow_size']);
                if ( isset($settings['ic_arrow_br'])    ) $clean['ic_arrow_br']    = $dim($settings['ic_arrow_br']);
                // Dots
                if ( isset($settings['ic_dot_color'])        ) $clean['ic_dot_color']        = $col($settings['ic_dot_color']);
                if ( isset($settings['ic_dot_active_color']) ) $clean['ic_dot_active_color'] = $col($settings['ic_dot_active_color']);
                if ( isset($settings['ic_dot_size'])         ) $clean['ic_dot_size']         = $dim($settings['ic_dot_size']);
                // Card
                if ( isset($settings['ic_card_bg'])     ) $clean['ic_card_bg']     = $col($settings['ic_card_bg']);
                if ( isset($settings['ic_card_shadow'])  ) $clean['ic_card_shadow'] = sanitize_text_field($settings['ic_card_shadow']);
                if ( isset($settings['ic_card_br'])     ) $clean['ic_card_br']     = $dim($settings['ic_card_br']);
                if ( isset($settings['ic_card_pad'])    ) $clean['ic_card_pad']    = $dim($settings['ic_card_pad']);
                break;
            }

            case 'feature-card': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                $fw  = static fn( $v ) => in_array( (string)$v, ['100','200','300','400','500','600','700','800','900'], true ) ? (string)$v : '';
                $allowed_pos = [ 'top', 'left', 'right', 'center' ];
                $allowed_al  = [ 'left', 'center', 'right', 'justify' ];
                $allowed_bd  = [ 'none', 'solid', 'dashed', 'dotted', 'double' ];
                $allowed_tt  = [ 'none', 'uppercase', 'lowercase', 'capitalize' ];
                $allowed_td  = [ 'none', 'underline', 'overline', 'line-through' ];
                $allowed_fs  = [ 'normal', 'italic', 'oblique' ];

                if ( isset($settings['fc_iconPosition']) && in_array($settings['fc_iconPosition'], $allowed_pos, true) ) $clean['fc_iconPosition'] = $settings['fc_iconPosition'];
                if ( isset($settings['fc_align'])        && in_array($settings['fc_align'],        $allowed_al,  true) ) $clean['fc_align']        = $settings['fc_align'];
                if ( isset($settings['fc_iconSpacing'])  ) $clean['fc_iconSpacing']   = $dim( $settings['fc_iconSpacing'] );
                if ( isset($settings['fc_contentSpacing']) ) $clean['fc_contentSpacing'] = $dim( $settings['fc_contentSpacing'] );

                if ( isset($settings['fc_icon_color'])   ) $clean['fc_icon_color']   = $col( $settings['fc_icon_color'] );
                if ( isset($settings['fc_icon_bg'])      ) $clean['fc_icon_bg']      = $col( $settings['fc_icon_bg'] );
                if ( isset($settings['fc_icon_size'])    ) $clean['fc_icon_size']    = $dim( $settings['fc_icon_size'] );
                if ( isset($settings['fc_icon_padding']) ) $clean['fc_icon_padding'] = $dim( $settings['fc_icon_padding'] );
                if ( isset($settings['fc_icon_br'])      ) $clean['fc_icon_br']      = $dim( $settings['fc_icon_br'] );

                // Title typography
                if ( isset($settings['fc_title_color'])      ) $clean['fc_title_color']      = $col( $settings['fc_title_color'] );
                if ( isset($settings['fc_title_fontFamily']) ) $clean['fc_title_fontFamily']  = sanitize_text_field( $settings['fc_title_fontFamily'] );
                if ( isset($settings['fc_title_fontSize'])   ) $clean['fc_title_fontSize']    = $dim( $settings['fc_title_fontSize'] );
                $clean['fc_title_fontWeight']   = $fw( $settings['fc_title_fontWeight']   ?? '' );
                if ( isset($settings['fc_title_fontStyle'])    && in_array($settings['fc_title_fontStyle'],    $allowed_fs, true) ) $clean['fc_title_fontStyle']    = $settings['fc_title_fontStyle'];
                if ( isset($settings['fc_title_textTransform']) && in_array($settings['fc_title_textTransform'], $allowed_tt, true) ) $clean['fc_title_textTransform'] = $settings['fc_title_textTransform'];
                if ( isset($settings['fc_title_textDecoration']) && in_array($settings['fc_title_textDecoration'], $allowed_td, true) ) $clean['fc_title_textDecoration'] = $settings['fc_title_textDecoration'];
                if ( isset($settings['fc_title_lineHeight'])   ) $clean['fc_title_lineHeight']   = $dim( $settings['fc_title_lineHeight'] );
                if ( isset($settings['fc_title_letterSpacing']) ) $clean['fc_title_letterSpacing'] = $dim( $settings['fc_title_letterSpacing'] );

                // Description typography
                if ( isset($settings['fc_desc_color'])      ) $clean['fc_desc_color']      = $col( $settings['fc_desc_color'] );
                if ( isset($settings['fc_desc_fontFamily']) ) $clean['fc_desc_fontFamily']  = sanitize_text_field( $settings['fc_desc_fontFamily'] );
                if ( isset($settings['fc_desc_fontSize'])   ) $clean['fc_desc_fontSize']    = $dim( $settings['fc_desc_fontSize'] );
                $clean['fc_desc_fontWeight']   = $fw( $settings['fc_desc_fontWeight']   ?? '' );
                if ( isset($settings['fc_desc_fontStyle'])    && in_array($settings['fc_desc_fontStyle'],    $allowed_fs, true) ) $clean['fc_desc_fontStyle']    = $settings['fc_desc_fontStyle'];
                if ( isset($settings['fc_desc_textTransform']) && in_array($settings['fc_desc_textTransform'], $allowed_tt, true) ) $clean['fc_desc_textTransform'] = $settings['fc_desc_textTransform'];
                if ( isset($settings['fc_desc_textDecoration']) && in_array($settings['fc_desc_textDecoration'], $allowed_td, true) ) $clean['fc_desc_textDecoration'] = $settings['fc_desc_textDecoration'];
                if ( isset($settings['fc_desc_lineHeight'])   ) $clean['fc_desc_lineHeight']   = $dim( $settings['fc_desc_lineHeight'] );
                if ( isset($settings['fc_desc_letterSpacing']) ) $clean['fc_desc_letterSpacing'] = $dim( $settings['fc_desc_letterSpacing'] );

                // Box style
                if ( isset($settings['fc_bg']) ) $clean['fc_bg'] = $col( $settings['fc_bg'] );
                if ( isset($settings['fc_bdType']) && in_array($settings['fc_bdType'], $allowed_bd, true) ) $clean['fc_bdType'] = $settings['fc_bdType'];
                foreach ( ['top','right','bottom','left'] as $side ) {
                    if ( isset($settings["fc_bdw_{$side}"]) ) $clean["fc_bdw_{$side}"] = $dim( $settings["fc_bdw_{$side}"] );
                    if ( isset($settings["fc_br_{$side}"])  ) $clean["fc_br_{$side}"]  = $dim( $settings["fc_br_{$side}"] );
                    if ( isset($settings["fc_pad_{$side}"]) ) $clean["fc_pad_{$side}"] = $dim( $settings["fc_pad_{$side}"] );
                }
                if ( isset($settings['fc_bdColor']) ) $clean['fc_bdColor'] = $col( $settings['fc_bdColor'] );
                break;
            }

            case 'icon-list': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );

                if ( isset( $settings['il_gap'] ) )          $clean['il_gap'] = $dim( $settings['il_gap'] );
                if ( isset( $settings['il_divider'] ) )      $clean['il_divider'] = (bool) $settings['il_divider'];
                if ( isset( $settings['il_div_color'] ) )    $clean['il_div_color'] = $col( $settings['il_div_color'] );
                if ( isset( $settings['il_div_weight'] ) )   $clean['il_div_weight'] = $dim( $settings['il_div_weight'] );
                $allowed_div_styles = [ 'solid', 'dashed', 'dotted' ];
                if ( isset( $settings['il_div_style'] ) && in_array( $settings['il_div_style'], $allowed_div_styles, true ) ) {
                    $clean['il_div_style'] = $settings['il_div_style'];
                }
                if ( isset( $settings['il_icon_color'] ) )   $clean['il_icon_color'] = $col( $settings['il_icon_color'] );
                if ( isset( $settings['il_icon_bg'] ) )      $clean['il_icon_bg'] = $col( $settings['il_icon_bg'] );
                if ( isset( $settings['il_icon_size'] ) )    $clean['il_icon_size'] = $dim( $settings['il_icon_size'] );
                if ( isset( $settings['il_icon_br'] ) )      $clean['il_icon_br'] = $dim( $settings['il_icon_br'] );
                if ( isset( $settings['il_icon_spacing'] ) ) $clean['il_icon_spacing'] = $dim( $settings['il_icon_spacing'] );
                if ( isset( $settings['il_text_color'] ) )      $clean['il_text_color']      = $col( $settings['il_text_color'] );
                if ( isset( $settings['il_text_fontFamily'] ) ) $clean['il_text_fontFamily']  = sanitize_text_field( $settings['il_text_fontFamily'] );
                if ( isset( $settings['il_text_fontSize'] ) )   $clean['il_text_fontSize']    = $dim( $settings['il_text_fontSize'] );
                $allowed_fw = [ '100', '200', '300', '400', '500', '600', '700', '800', '900' ];
                if ( isset( $settings['il_text_fontWeight'] ) && in_array( (string) $settings['il_text_fontWeight'], $allowed_fw, true ) ) {
                    $clean['il_text_fontWeight'] = $settings['il_text_fontWeight'];
                }
                $allowed_fs = [ 'normal', 'italic', 'oblique' ];
                if ( isset( $settings['il_text_fontStyle'] ) && in_array( $settings['il_text_fontStyle'], $allowed_fs, true ) ) {
                    $clean['il_text_fontStyle'] = $settings['il_text_fontStyle'];
                }
                $allowed_tt = [ 'none', 'uppercase', 'lowercase', 'capitalize' ];
                if ( isset( $settings['il_text_textTransform'] ) && in_array( $settings['il_text_textTransform'], $allowed_tt, true ) ) {
                    $clean['il_text_textTransform'] = $settings['il_text_textTransform'];
                }
                $allowed_td = [ 'none', 'underline', 'overline', 'line-through' ];
                if ( isset( $settings['il_text_textDecoration'] ) && in_array( $settings['il_text_textDecoration'], $allowed_td, true ) ) {
                    $clean['il_text_textDecoration'] = $settings['il_text_textDecoration'];
                }
                if ( isset( $settings['il_text_lineHeight'] ) )    $clean['il_text_lineHeight']   = $dim( $settings['il_text_lineHeight'] );
                if ( isset( $settings['il_text_letterSpacing'] ) ) $clean['il_text_letterSpacing'] = $dim( $settings['il_text_letterSpacing'] );
                break;
            }

            // ── Testimonials ────────────────────────────────────────────────
            case 'testimonials': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                $txt = static fn( $v ) => sanitize_text_field( (string) $v );
                $allowed_fw = [ '100','200','300','400','500','600','700','800','900' ];
                $allowed_tt = [ 'none','uppercase','lowercase','capitalize' ];

                // Colors
                foreach ( [ 'ts_card_bg', 'ts_bd_color', 'ts_quote_color', 'ts_qmark_color',
                             'ts_name_color', 'ts_role_color', 'ts_star_color', 'ts_star_empty' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $v = $col( $settings[ $k ] );
                        if ( $v !== '' ) $clean[ $k ] = $v;
                    }
                }
                // Dimensions
                foreach ( [ 'ts_card_radius', 'ts_card_pad', 'ts_gap', 'ts_bd_width', 'ts_qmark_size',
                             'ts_avatar_size', 'ts_avatar_radius', 'ts_star_size',
                             // Typography dimensions
                             'ts_q_fontSize', 'ts_q_lineHeight', 'ts_q_letterSpacing',
                             'ts_name_fontSize', 'ts_name_lineHeight', 'ts_name_letterSpacing',
                             'ts_role_fontSize', 'ts_role_lineHeight', 'ts_role_letterSpacing' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $v = $dim( $settings[ $k ] );
                        if ( $v !== '' ) $clean[ $k ] = $v;
                    }
                }
                // Font families (free text, sanitized)
                foreach ( [ 'ts_q_fontFamily', 'ts_name_fontFamily', 'ts_role_fontFamily' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && $settings[ $k ] !== '' ) {
                        $clean[ $k ] = $txt( $settings[ $k ] );
                    }
                }
                // Font weights
                foreach ( [ 'ts_q_fontWeight', 'ts_name_fontWeight', 'ts_role_fontWeight' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && in_array( (string) $settings[ $k ], $allowed_fw, true ) ) {
                        $clean[ $k ] = $settings[ $k ];
                    }
                }
                // Text transforms
                foreach ( [ 'ts_q_textTransform', 'ts_name_textTransform', 'ts_role_textTransform' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && in_array( $settings[ $k ], $allowed_tt, true ) ) {
                        $clean[ $k ] = $settings[ $k ];
                    }
                }
                // Enum allowlists
                if ( isset( $settings['ts_card_shadow'] ) && in_array( $settings['ts_card_shadow'], [ 'none','sm','md','lg','xl' ], true ) ) {
                    $clean['ts_card_shadow'] = $settings['ts_card_shadow'];
                }
                if ( isset( $settings['ts_align'] ) && in_array( $settings['ts_align'], [ 'left','center','right' ], true ) ) {
                    $clean['ts_align'] = $settings['ts_align'];
                }
                if ( isset( $settings['ts_bd_type'] ) && in_array( $settings['ts_bd_type'], [ 'none','solid','dashed','dotted' ], true ) ) {
                    $clean['ts_bd_type'] = $settings['ts_bd_type'];
                }
                if ( isset( $settings['ts_quote_fstyle'] ) && in_array( $settings['ts_quote_fstyle'], [ 'normal','italic' ], true ) ) {
                    $clean['ts_quote_fstyle'] = $settings['ts_quote_fstyle'];
                }
                break;
            }

            // ── CTA ───────────────────────────────────────────────────────────
            case 'cta': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );
                $txt = static fn( $v ) => sanitize_text_field( (string) $v );
                $allowed_fw = [ '100','200','300','400','500','600','700','800','900' ];
                $allowed_tt = [ 'none','uppercase','lowercase','capitalize' ];
                $allowed_bd = [ 'none','solid','dashed','dotted' ];
                $allowed_al = [ 'left','center','right' ];

                // Colors
                foreach ( [
                    'cta_bg', 'cta_bg_hover', 'cta_bd_color', 'cta_bd_color_hover',
                    'cta_title_color', 'cta_sub_color',
                    'cta_pbtn_bg', 'cta_pbtn_bg_hover', 'cta_pbtn_color', 'cta_pbtn_color_hover',
                    'cta_pbtn_bd_color', 'cta_pbtn_bd_color_hover',
                    'cta_sbtn_bg', 'cta_sbtn_bg_hover', 'cta_sbtn_color', 'cta_sbtn_color_hover',
                    'cta_sbtn_bd_color', 'cta_sbtn_bd_color_hover',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $v = $col( $settings[ $k ] );
                        if ( $v !== '' ) $clean[ $k ] = $v;
                    }
                }

                // Dimensions
                foreach ( [
                    // Wrapper padding (per-side)
                    'cta_pad_top', 'cta_pad_right', 'cta_pad_bottom', 'cta_pad_left',
                    // Wrapper border radius (per-corner)
                    'cta_br_top', 'cta_br_right', 'cta_br_bottom', 'cta_br_left',
                    // Wrapper border width (per-side)
                    'cta_bdw_top', 'cta_bdw_right', 'cta_bdw_bottom', 'cta_bdw_left',
                    // Primary button padding
                    'cta_pbtn_pad_top', 'cta_pbtn_pad_right', 'cta_pbtn_pad_bottom', 'cta_pbtn_pad_left',
                    // Primary button border radius
                    'cta_pbtn_br_top', 'cta_pbtn_br_right', 'cta_pbtn_br_bottom', 'cta_pbtn_br_left',
                    // Primary button border width
                    'cta_pbtn_bdw_top', 'cta_pbtn_bdw_right', 'cta_pbtn_bdw_bottom', 'cta_pbtn_bdw_left',
                    // Secondary button padding
                    'cta_sbtn_pad_top', 'cta_sbtn_pad_right', 'cta_sbtn_pad_bottom', 'cta_sbtn_pad_left',
                    // Secondary button border radius
                    'cta_sbtn_br_top', 'cta_sbtn_br_right', 'cta_sbtn_br_bottom', 'cta_sbtn_br_left',
                    // Secondary button border width
                    'cta_sbtn_bdw_top', 'cta_sbtn_bdw_right', 'cta_sbtn_bdw_bottom', 'cta_sbtn_bdw_left',
                    // Typography
                    'cta_title_fontSize', 'cta_title_lineHeight', 'cta_title_letterSpacing',
                    'cta_sub_fontSize',   'cta_sub_lineHeight',   'cta_sub_letterSpacing',
                    'cta_pbtn_fontSize',  'cta_pbtn_lineHeight',  'cta_pbtn_letterSpacing',
                    'cta_sbtn_fontSize',  'cta_sbtn_lineHeight',  'cta_sbtn_letterSpacing',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $v = $dim( $settings[ $k ] );
                        if ( $v !== '' ) $clean[ $k ] = $v;
                    }
                }

                // Font families
                foreach ( [ 'cta_title_fontFamily','cta_sub_fontFamily','cta_pbtn_fontFamily','cta_sbtn_fontFamily' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && $settings[ $k ] !== '' ) {
                        $clean[ $k ] = $txt( $settings[ $k ] );
                    }
                }

                // Font weights
                foreach ( [ 'cta_title_fontWeight','cta_sub_fontWeight','cta_pbtn_fontWeight','cta_sbtn_fontWeight' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && in_array( (string) $settings[ $k ], $allowed_fw, true ) ) {
                        $clean[ $k ] = $settings[ $k ];
                    }
                }

                // Text transforms
                foreach ( [ 'cta_title_textTransform','cta_sub_textTransform','cta_pbtn_textTransform','cta_sbtn_textTransform' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && in_array( $settings[ $k ], $allowed_tt, true ) ) {
                        $clean[ $k ] = $settings[ $k ];
                    }
                }

                // Box shadows (raw CSS strings)
                foreach ( [ 'cta_shadow','cta_pbtn_shadow','cta_sbtn_shadow' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && $settings[ $k ] !== '' ) {
                        $clean[ $k ] = $txt( $settings[ $k ] );
                    }
                }

                // Enum: border types
                foreach ( [ 'cta_bd_type','cta_pbtn_bd_type','cta_sbtn_bd_type' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && in_array( $settings[ $k ], $allowed_bd, true ) ) {
                        $clean[ $k ] = $settings[ $k ];
                    }
                }

                // Enum: alignment
                foreach ( [ 'cta_align','cta_btn_align' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && in_array( $settings[ $k ], $allowed_al, true ) ) {
                        $clean[ $k ] = $settings[ $k ];
                    }
                }

                break;
            }

            // ── WooCommerce Single Product ───────────────────────────────────
            case 'single-product': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );

                // Strict CSS filter — sanitize_css_value() alone permits braces
                // and semicolons, which would let a value escape its rule.
                $css_safe = static function ( $v ): string {
                    $v = self::sanitize_css_value( (string) $v );
                    if ( '' === $v ) {
                        return '';
                    }
                    if ( preg_match( '/[{};@\\\\<>]/', $v ) ) {
                        return '';
                    }
                    if ( false !== strpos( $v, '/*' ) || false !== strpos( $v, '*/' ) ) {
                        return '';
                    }
                    return preg_match( '/^[A-Za-z0-9_\-\s.,%#()\/]+$/', $v ) ? $v : '';
                };

                // Gallery column width is a percentage of the main grid.
                if ( isset( $settings['sp_gallery_width'] ) ) {
                    $clean['sp_gallery_width'] = (string) max( 20, min( 80, intval( $settings['sp_gallery_width'] ) ) );
                }

                foreach ( [
                    'sp_col_gap', 'sp_col_gap_tablet', 'sp_radius', 'sp_bd_width',
                    'sp_img_radius', 'sp_thumb_gap', 'sp_thumb_size', 'sp_thumb_radius',
                    'sp_title_size', 'sp_title_weight', 'sp_title_lh', 'sp_title_ls',
                    'sp_star_size', 'sp_rating_spacing', 'sp_review_size',
                    'sp_price_size', 'sp_price_weight', 'sp_price_spacing',
                    'sp_badge_size', 'sp_badge_radius',
                    'sp_desc_size', 'sp_desc_lh', 'sp_desc_spacing',
                    'sp_cart_spacing', 'sp_qty_width', 'sp_qty_height', 'sp_qty_radius', 'sp_qty_size',
                    'sp_btn_size', 'sp_btn_weight', 'sp_btn_radius', 'sp_btn_bd_width',
                    'sp_meta_size', 'sp_meta_spacing', 'sp_share_size',
                    'sp_tabs_spacing', 'sp_tab_gap', 'sp_tab_size', 'sp_tab_weight',
                    'sp_tab_content_size', 'sp_tab_content_lh', 'sp_section_title_size',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) && '' !== $settings[ $k ] ) {
                        $clean[ $k ] = $dim( $settings[ $k ] );
                    }
                }

                foreach ( [
                    'sp_pad', 'sp_margin', 'sp_shadow', 'sp_title_margin',
                    'sp_title_font', 'sp_badge_pad', 'sp_btn_pad',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) && '' !== $settings[ $k ] ) {
                        $clean[ $k ] = $css_safe( $settings[ $k ] );
                    }
                }

                foreach ( [
                    'sp_bg', 'sp_bd_color', 'sp_thumb_active_color',
                    'sp_title_color', 'sp_title_hover_color',
                    'sp_star_color', 'sp_star_empty_color', 'sp_review_color',
                    'sp_price_color', 'sp_sale_price_color', 'sp_badge_bg', 'sp_badge_color',
                    'sp_desc_color',
                    'sp_qty_bg', 'sp_qty_bd_color', 'sp_qty_color', 'sp_qty_btn_color',
                    'sp_btn_color', 'sp_btn_bg', 'sp_btn_bd_color',
                    'sp_btn_hover_color', 'sp_btn_hover_bg', 'sp_btn_hover_bd',
                    'sp_meta_color', 'sp_meta_label_color',
                    'sp_share_bg', 'sp_share_color', 'sp_share_hover_bg',
                    'sp_tab_color', 'sp_tab_active_color', 'sp_tab_hover_color',
                    'sp_tab_bd_color', 'sp_tab_content_color',
                    'sp_section_title_color', 'sp_empty_color',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) && '' !== $settings[ $k ] ) {
                        $clean[ $k ] = $col( $settings[ $k ] );
                    }
                }

                $enums = [
                    'sp_bd_type'         => [ 'none', 'solid', 'dashed', 'dotted', 'double' ],
                    'sp_img_hover'       => [ 'none', 'zoom', 'lift' ],
                    'sp_title_align'     => [ 'inherit', 'left', 'center', 'right' ],
                    'sp_title_transform' => [ 'none', 'uppercase', 'lowercase', 'capitalize' ],
                    'sp_desc_align'      => [ 'inherit', 'left', 'center', 'right', 'justify' ],
                    'sp_btn_bd_type'     => [ 'none', 'solid', 'dashed', 'dotted' ],
                    'sp_btn_hover_anim'  => [ 'none', 'grow', 'shrink', 'float', 'sink' ],
                    'sp_btn_icon'        => [ 'none', 'cart', 'bag', 'plus', 'arrow' ],
                    'sp_btn_icon_pos'    => [ 'before', 'after' ],
                    'sp_share_style'     => [ 'circle', 'rounded', 'square' ],
                ];
                foreach ( $enums as $k => $allowed ) {
                    if ( isset( $settings[ $k ] ) && in_array( $settings[ $k ], $allowed, true ) ) {
                        $clean[ $k ] = $settings[ $k ];
                    }
                }

                if ( isset( $settings['sp_img_size'] ) ) {
                    $clean['sp_img_size'] = sanitize_key( (string) $settings['sp_img_size'] );
                }

                break;
            }

            // ── WooCommerce Product Categories ───────────────────────────────
            case 'product-categories': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );

                // Same strict CSS filter as products-grid: sanitize_css_value()
                // alone permits braces/semicolons, which would let a value break
                // out of its declaration block. The element re-checks too.
                $css_safe = static function ( $v ): string {
                    $v = self::sanitize_css_value( (string) $v );
                    if ( '' === $v ) {
                        return '';
                    }
                    if ( preg_match( '/[{};@\\\\<>]/', $v ) ) {
                        return '';
                    }
                    if ( false !== strpos( $v, '/*' ) || false !== strpos( $v, '*/' ) ) {
                        return '';
                    }
                    return preg_match( '/^[A-Za-z0-9_\-\s.,%#()\/]+$/', $v ) ? $v : '';
                };

                foreach ( [ 'pc_cols_desktop', 'pc_cols_tablet', 'pc_cols_mobile' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = (string) max( 1, min( 6, intval( $settings[ $k ] ) ) );
                    }
                }

                foreach ( [
                    'pc_col_gap', 'pc_row_gap', 'pc_card_radius', 'pc_card_bd_width',
                    'pc_card_hover_lift', 'pc_card_transition', 'pc_img_radius',
                    'pc_img_width', 'pc_img_height', 'pc_title_size', 'pc_title_weight',
                    'pc_title_lh', 'pc_title_ls', 'pc_count_size', 'pc_count_radius',
                    'pc_desc_size', 'pc_desc_spacing', 'pc_btn_size', 'pc_btn_weight',
                    'pc_btn_radius', 'pc_btn_bd_width', 'pc_btn_spacing',
                    'pc_arrow_size', 'pc_dot_spacing',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) && '' !== $settings[ $k ] ) {
                        $clean[ $k ] = $dim( $settings[ $k ] );
                    }
                }

                // Free-text CSS (shorthands, shadows, font family).
                foreach ( [
                    'pc_card_pad', 'pc_card_margin', 'pc_body_pad', 'pc_count_pad',
                    'pc_btn_pad', 'pc_title_margin', 'pc_card_shadow',
                    'pc_card_hover_shadow', 'pc_title_font',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) && '' !== $settings[ $k ] ) {
                        $clean[ $k ] = $css_safe( $settings[ $k ] );
                    }
                }

                foreach ( [
                    'pc_card_bg', 'pc_card_hover_bg', 'pc_card_bd_color',
                    'pc_card_hover_bd_color', 'pc_img_overlay_color', 'pc_overlay_color',
                    'pc_title_color', 'pc_title_hover_color', 'pc_count_color',
                    'pc_count_bg', 'pc_desc_color', 'pc_btn_color', 'pc_btn_bg',
                    'pc_btn_bd_color', 'pc_btn_hover_color', 'pc_btn_hover_bg',
                    'pc_btn_hover_bd', 'pc_arrow_color', 'pc_arrow_bg',
                    'pc_dot_color', 'pc_dot_active_color', 'pc_empty_color',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) && '' !== $settings[ $k ] ) {
                        $clean[ $k ] = $col( $settings[ $k ] );
                    }
                }

                $enums = [
                    'pc_align'           => [ 'left', 'center', 'right' ],
                    'pc_card_bd_type'    => [ 'none', 'solid', 'dashed', 'dotted', 'double' ],
                    'pc_img_fit'         => [ 'cover', 'contain', 'fill', 'none', 'scale-down' ],
                    'pc_img_hover'       => [ 'none', 'zoom', 'fade', 'overlay', 'grayscale' ],
                    'pc_title_transform' => [ 'none', 'uppercase', 'lowercase', 'capitalize' ],
                    'pc_desc_align'      => [ 'inherit', 'left', 'center', 'right' ],
                    'pc_btn_bd_type'     => [ 'none', 'solid', 'dashed', 'dotted' ],
                    'pc_btn_hover_anim'  => [ 'none', 'grow', 'shrink', 'float', 'sink' ],
                    'pc_btn_icon'        => [ 'none', 'arrow', 'chevron', 'plus', 'cart' ],
                    'pc_btn_icon_pos'    => [ 'before', 'after' ],
                ];
                foreach ( $enums as $k => $allowed ) {
                    if ( isset( $settings[ $k ] ) && in_array( $settings[ $k ], $allowed, true ) ) {
                        $clean[ $k ] = $settings[ $k ];
                    }
                }

                if ( isset( $settings['pc_img_ratio'] ) ) {
                    $r = (string) $settings['pc_img_ratio'];
                    if ( preg_match( '#^\d+(\.\d+)?/\d+(\.\d+)?$#', $r ) || 'auto' === $r ) {
                        $clean['pc_img_ratio'] = $r;
                    }
                }

                if ( isset( $settings['pc_img_size'] ) ) {
                    $clean['pc_img_size'] = sanitize_key( (string) $settings['pc_img_size'] );
                }

                break;
            }

            // ── WooCommerce Products Grid ────────────────────────────────────
            case 'products-grid': {
                $dim = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col = static fn( $v ) => self::sanitize_color( (string) $v );

                // Columns are hard-bounded 1–6 (matches the widget's control).
                foreach ( [ 'pg_cols_desktop', 'pg_cols_tablet', 'pg_cols_mobile' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $clean[ $k ] = (string) max( 1, min( 6, intval( $settings[ $k ] ) ) );
                    }
                }

                // Numeric / dimension-ish values. sanitize_dimension() keeps a
                // bare number or a valid CSS length and drops anything else.
                foreach ( [
                    'pg_col_gap', 'pg_row_gap', 'pg_card_radius', 'pg_card_bd_width',
                    'pg_card_hover_lift', 'pg_card_transition', 'pg_img_radius',
                    'pg_img_width', 'pg_img_height', 'pg_badge_radius', 'pg_badge_size',
                    'pg_badge_weight', 'pg_title_size', 'pg_title_weight', 'pg_price_size',
                    'pg_price_weight', 'pg_price_spacing', 'pg_star_size', 'pg_rating_spacing',
                    'pg_btn_size', 'pg_btn_weight', 'pg_btn_radius', 'pg_btn_bd_width',
                    'pg_btn_spacing', 'pg_action_size', 'pg_pag_size', 'pg_pag_radius',
                    'pg_pag_spacing', 'pg_pag_margin',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) && '' !== $settings[ $k ] ) {
                        $clean[ $k ] = $dim( $settings[ $k ] );
                    }
                }

                // Free-text CSS values (shorthand padding/margin, box-shadow).
                // sanitize_css_value() alone permits braces and semicolons, so a
                // value could terminate its declaration and inject page-wide
                // rules. Reject any structural character outright — the element
                // applies the same check again before emitting CSS.
                $css_safe = static function ( $v ): string {
                    $v = self::sanitize_css_value( (string) $v );
                    if ( '' === $v ) {
                        return '';
                    }
                    if ( preg_match( '/[{};@\\\\<>]/', $v ) ) {
                        return '';
                    }
                    if ( false !== strpos( $v, '/*' ) || false !== strpos( $v, '*/' ) ) {
                        return '';
                    }
                    return preg_match( '/^[A-Za-z0-9_\-\s.,%#()\/]+$/', $v ) ? $v : '';
                };

                foreach ( [
                    'pg_card_pad', 'pg_body_pad', 'pg_badge_pad',
                    'pg_btn_pad', 'pg_title_margin',
                    'pg_card_shadow', 'pg_card_hover_shadow',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) && '' !== $settings[ $k ] ) {
                        $clean[ $k ] = $css_safe( $settings[ $k ] );
                    }
                }

                // Colors.
                foreach ( [
                    'pg_card_bg', 'pg_card_bd_color', 'pg_card_hover_bd_color',
                    'pg_img_overlay_color', 'pg_badge_bg', 'pg_badge_color',
                    'pg_title_color', 'pg_title_hover_color', 'pg_price_color',
                    'pg_sale_price_color', 'pg_star_color', 'pg_star_empty_color',
                    'pg_btn_color', 'pg_btn_bg', 'pg_btn_bd_color', 'pg_btn_hover_color',
                    'pg_btn_hover_bg', 'pg_btn_hover_bd', 'pg_action_color',
                    'pg_action_bg', 'pg_action_hover_color', 'pg_pag_color',
                    'pg_pag_active_color', 'pg_pag_hover_color', 'pg_pag_bd_color',
                    'pg_empty_color',
                ] as $k ) {
                    if ( isset( $settings[ $k ] ) && '' !== $settings[ $k ] ) {
                        $clean[ $k ] = $col( $settings[ $k ] );
                    }
                }

                // Enums — anything unrecognized is dropped, not defaulted, so
                // the element's own fallback applies.
                $enums = [
                    'pg_card_align'     => [ 'left', 'center', 'right' ],
                    'pg_card_bd_type'   => [ 'none', 'solid', 'dashed', 'dotted', 'double' ],
                    'pg_img_fit'        => [ 'cover', 'contain', 'fill', 'none', 'scale-down' ],
                    'pg_img_hover'      => [ 'none', 'zoom', 'zoom-out', 'grayscale', 'blur', 'lift' ],
                    'pg_badge_pos'      => [ 'top-left', 'top-right', 'bottom-left', 'bottom-right' ],
                    'pg_btn_bd_type'    => [ 'none', 'solid', 'dashed', 'dotted' ],
                    'pg_btn_hover_anim' => [ 'none', 'grow', 'shrink', 'float', 'sink' ],
                    'pg_btn_icon'       => [ 'none', 'cart', 'bag', 'plus', 'arrow' ],
                    'pg_btn_icon_pos'   => [ 'before', 'after' ],
                ];
                foreach ( $enums as $k => $allowed ) {
                    if ( isset( $settings[ $k ] ) && in_array( $settings[ $k ], $allowed, true ) ) {
                        $clean[ $k ] = $settings[ $k ];
                    }
                }

                // Image ratio: "W/H" or "auto".
                if ( isset( $settings['pg_img_ratio'] ) ) {
                    $r = (string) $settings['pg_img_ratio'];
                    if ( preg_match( '#^\d+(\.\d+)?/\d+(\.\d+)?$#', $r ) || 'auto' === $r ) {
                        $clean['pg_img_ratio'] = $r;
                    }
                }

                // Registered image size key.
                if ( isset( $settings['pg_img_size'] ) ) {
                    $clean['pg_img_size'] = sanitize_key( (string) $settings['pg_img_size'] );
                }

                break;
            }

            // ── Gallery ──────────────────────────────────────────────────────
            case 'gallery': {
                $dim     = static fn( $v ) => self::sanitize_dimension( (string) $v );
                $col     = static fn( $v ) => self::sanitize_color( (string) $v );
                $txt     = static fn( $v ) => sanitize_text_field( (string) $v );
                $allowed_bd = [ 'none','solid','dashed','dotted' ];
                $allowed_al = [ 'left','center','right' ];
                $allowed_hv = [ 'none','zoom','scale','fade','overlay' ];
                $allowed_cs = [ 'always','hover','never' ];

                // Columns & gap
                foreach ( [ 'gal_cols_desktop','gal_cols_tablet','gal_cols_mobile' ] as $k ) {
                    if ( isset( $settings[ $k ] ) ) {
                        $v = max( 1, min( 12, intval( $settings[ $k ] ) ) );
                        $clean[ $k ] = (string) $v;
                    }
                }
                foreach ( [ 'gal_gap','gal_caption_size' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && $settings[ $k ] !== '' ) {
                        $clean[ $k ] = $dim( $settings[ $k ] ) ?: $settings[ $k ];
                    }
                }

                // Image ratio (e.g. "1/1", "4/3", "16/9", "auto")
                if ( isset( $settings['gal_img_ratio'] ) ) {
                    $r = $settings['gal_img_ratio'];
                    $clean['gal_img_ratio'] = ( preg_match('#^\d+/\d+$#', $r) || $r === 'auto' ) ? $r : '1/1';
                }

                // Hover effect
                if ( isset( $settings['gal_hover'] ) && in_array( $settings['gal_hover'], $allowed_hv, true ) ) {
                    $clean['gal_hover'] = $settings['gal_hover'];
                }

                // Overlay
                foreach ( [ 'gal_overlay_color','gal_caption_color','gal_caption_bg','gal_img_bd_color' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && $settings[ $k ] !== '' ) {
                        $clean[ $k ] = $col( $settings[ $k ] );
                    }
                }
                if ( isset( $settings['gal_overlay_opacity'] ) && is_numeric( $settings['gal_overlay_opacity'] ) ) {
                    $clean['gal_overlay_opacity'] = (string) max( 0, min( 1, (float) $settings['gal_overlay_opacity'] ) );
                }

                // Caption show
                if ( isset( $settings['gal_caption_show'] ) && in_array( $settings['gal_caption_show'], $allowed_cs, true ) ) {
                    $clean['gal_caption_show'] = $settings['gal_caption_show'];
                }
                if ( isset( $settings['gal_caption_align'] ) && in_array( $settings['gal_caption_align'], $allowed_al, true ) ) {
                    $clean['gal_caption_align'] = $settings['gal_caption_align'];
                }

                // Border-radius (per side)
                foreach ( [ 'gal_img_br_top','gal_img_br_right','gal_img_br_bottom','gal_img_br_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && $settings[ $k ] !== '' ) {
                        $clean[ $k ] = $dim( $settings[ $k ] ) ?: $settings[ $k ];
                    }
                }

                // Border type & width
                if ( isset( $settings['gal_img_bd_type'] ) && in_array( $settings['gal_img_bd_type'], $allowed_bd, true ) ) {
                    $clean['gal_img_bd_type'] = $settings['gal_img_bd_type'];
                }
                foreach ( [ 'gal_img_bdw_top','gal_img_bdw_right','gal_img_bdw_bottom','gal_img_bdw_left' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && $settings[ $k ] !== '' ) {
                        $clean[ $k ] = $dim( $settings[ $k ] ) ?: $settings[ $k ];
                    }
                }

                // Shadow (raw CSS, text-sanitized)
                if ( isset( $settings['gal_img_shadow'] ) && $settings['gal_img_shadow'] !== '' ) {
                    $clean['gal_img_shadow'] = $txt( $settings['gal_img_shadow'] );
                }

                // Filter bar colors
                foreach ( [ 'gal_filter_color','gal_filter_bg','gal_filter_active_color','gal_filter_active_bg' ] as $k ) {
                    if ( isset( $settings[ $k ] ) && $settings[ $k ] !== '' ) {
                        $clean[ $k ] = $col( $settings[ $k ] );
                    }
                }

                break;
            }
        }

        return $clean;
    }

    // -------------------------------------------------------------------------
    // Style-map sanitizer
    // -------------------------------------------------------------------------

    /**
     * Sanitize a node's styles array.
     * Each CSS property has a targeted validator so garbage/injection cannot survive.
     *
     * @param  array $styles Raw styles map.
     * @return array         Sanitized styles map.
     */
    public static function sanitize_styles( array $styles ): array {
        $clean = [];

        $color_keys = [
            'color', 'backgroundColor', 'textStrokeColor', 'textShadowColor',
            // Gradient colors (normal + hover)
            'gradientColor1', 'gradientColor2',
            'gradientColor1Hover', 'gradientColor2Hover',
            // Hover overrides
            'colorHover', 'backgroundColorHover', 'borderColorHover',
            // Icon element — style tab custom keys
            'iconColorHover', 'iconBoxColor', 'iconBoxColorHover',
        ];
        $dim_keys   = [
            'fontSize', 'lineHeight', 'letterSpacing', 'wordSpacing',
            'marginTop', 'marginBottom', 'marginLeft', 'marginRight',
            'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight',
            'width', 'height', 'minHeight',
            'borderRadius',
            'borderTopLeftRadius', 'borderTopRightRadius',
            'borderBottomRightRadius', 'borderBottomLeftRadius',
            'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
            'textStrokeWidth', 'textShadowH', 'textShadowV', 'textShadowBlur',
            // Outer wrapper padding (Advanced tab — independent of element's own padding)
            'outerPaddingTop', 'outerPaddingRight', 'outerPaddingBottom', 'outerPaddingLeft',
            // Icon element — style tab custom keys
            'iconSize', 'iconPadding',
        ];

        foreach ( $styles as $key => $raw ) {
            if ( '' === (string) $raw ) {
                continue;
            }
            $val = (string) $raw;

            if ( in_array( $key, $color_keys, true ) ) {
                $v = self::sanitize_color( $val );
                if ( $v ) $clean[ $key ] = $v;

            } elseif ( in_array( $key, $dim_keys, true ) ) {
                $v = self::sanitize_dimension( $val );
                if ( '' !== $v ) $clean[ $key ] = $v;

            } else {
                switch ( $key ) {
                    case 'fontFamily':
                        // Letters, digits, spaces, quotes, commas, hyphens — nothing else.
                        $v = preg_replace( '/[^a-zA-Z0-9\s,\-_"\'.()]/', '', $val );
                        if ( $v ) $clean[ $key ] = $v;
                        break;

                    case 'fontWeight':
                        if ( in_array( $val, [ 'normal','bold','bolder','lighter' ], true )
                             || preg_match( '/^\d{3}$/', $val ) ) {
                            $clean[ $key ] = $val;
                        }
                        break;

                    case 'fontStyle':
                        if ( in_array( $val, [ 'normal','italic','oblique' ], true ) ) {
                            $clean[ $key ] = $val;
                        }
                        break;

                    case 'textAlign':
                        if ( in_array( $val, [ 'left','center','right','justify' ], true ) ) {
                            $clean[ $key ] = $val;
                        }
                        break;

                    case 'textDecoration':
                        if ( in_array( $val, [ 'none','underline','line-through','overline' ], true ) ) {
                            $clean[ $key ] = $val;
                        }
                        break;

                    case 'textTransform':
                        if ( in_array( $val, [ 'none','uppercase','lowercase','capitalize' ], true ) ) {
                            $clean[ $key ] = $val;
                        }
                        break;

                    case 'display':
                        if ( in_array( $val, [ 'block','inline','inline-block','flex','grid','none' ], true ) ) {
                            $clean[ $key ] = $val;
                        }
                        break;

                    case 'opacity':
                        $f = (float) $val;
                        if ( $f >= 0.0 && $f <= 1.0 ) {
                            $clean[ $key ] = (string) $f;
                        }
                        break;

                    case 'border':
                    case 'boxShadow':
                    case 'boxShadowHover':
                        $v = self::sanitize_css_value( $val );
                        if ( $v ) $clean[ $key ] = $v;
                        break;

                    case 'backgroundType':
                        if ( in_array( $val, [ 'classic', 'gradient' ], true ) ) {
                            $clean[ $key ] = $val;
                        }
                        break;

                    case 'gradientAngle':
                        // e.g. "90deg" or "180deg"
                        if ( preg_match( '/^\d+deg$/', $val ) ) {
                            $clean[ $key ] = $val;
                        }
                        break;

                    case 'borderStyle':
                        if ( in_array( $val, [ 'none','solid','dashed','dotted','double','groove','ridge','inset','outset' ], true ) ) {
                            $clean[ $key ] = $val;
                        }
                        break;

                    case 'borderColor':
                        $v = self::sanitize_color( $val );
                        if ( $v ) $clean[ $key ] = $v;
                        break;

                    case 'iconRotate':
                        // e.g. "45deg" or "180deg"
                        if ( preg_match( '/^\d+(\.\d+)?deg$/', $val ) ) {
                            $clean[ $key ] = $val;
                        }
                        break;
                }
            }
        }

        return $clean;
    }

    // -------------------------------------------------------------------------
    // Background data sanitizer
    // -------------------------------------------------------------------------

    public static function sanitize_bg_data( array $bg ): array {
        if ( empty( $bg ) ) {
            return [];
        }

        $allowed_types = [ 'classic', 'gradient', 'video', 'slideshow', 'color', 'image' ];
        $type_raw = $bg['type'] ?? 'classic';
        $type     = in_array( $type_raw, $allowed_types, true ) ? $type_raw : 'classic';
        $clean = [ 'type' => $type ];

        switch ( $type ) {
            case 'classic':
            case 'color':
            case 'image':
                if ( ! empty( $bg['color'] ) ) {
                    $c = self::sanitize_color( $bg['color'] );
                    if ( $c ) $clean['color'] = $c;
                }
                if ( ! empty( $bg['imageUrl'] ) ) {
                    $clean['imageUrl'] = self::sanitize_url( $bg['imageUrl'] );
                    $clean['imagePosition'] = in_array(
                        $bg['imagePosition'] ?? 'center center',
                        [ 'center center','center top','center bottom','left center','right center',
                          'left top','left bottom','right top','right bottom' ], true
                    ) ? $bg['imagePosition'] : 'center center';
                    $clean['imageSize'] = in_array( $bg['imageSize'] ?? 'cover', [ 'cover','contain','auto' ], true )
                        ? $bg['imageSize'] : 'cover';
                }
                // Legacy fields
                if ( ! empty( $bg['value'] ) ) {
                    $c = self::sanitize_color( $bg['value'] );
                    if ( $c ) $clean['value'] = $c;
                }
                if ( ! empty( $bg['url'] ) ) {
                    $clean['url'] = self::sanitize_url( $bg['url'] );
                    $clean['position'] = sanitize_text_field( $bg['position'] ?? 'center center' );
                    $clean['size']     = sanitize_text_field( $bg['size']     ?? 'cover' );
                }
                break;

            case 'gradient':
                if ( ! empty( $bg['gradient'] ) ) {
                    $clean['gradient'] = self::sanitize_css_value( $bg['gradient'] );
                }
                if ( ! empty( $bg['gradientColor1'] ) ) {
                    $c = self::sanitize_color( $bg['gradientColor1'] );
                    if ( $c ) $clean['gradientColor1'] = $c;
                }
                if ( ! empty( $bg['gradientColor2'] ) ) {
                    $c = self::sanitize_color( $bg['gradientColor2'] );
                    if ( $c ) $clean['gradientColor2'] = $c;
                }
                $clean['gradientLocation1'] = min( 100, max( 0, intval( $bg['gradientLocation1'] ?? 0 ) ) );
                $clean['gradientLocation2'] = min( 100, max( 0, intval( $bg['gradientLocation2'] ?? 100 ) ) );
                $grad_type              = $bg['gradientType'] ?? 'linear';
                $clean['gradientType']  = in_array( $grad_type, [ 'linear','radial' ], true )
                                          ? $grad_type : 'linear';
                $clean['gradientAngle'] = min( 360, max( 0, intval( $bg['gradientAngle'] ?? 180 ) ) );
                break;

            case 'video':
                if ( ! empty( $bg['videoUrl'] ) ) {
                    $url = self::sanitize_url( $bg['videoUrl'] );
                    if ( $url ) $clean['videoUrl'] = $url;
                }
                break;

            case 'slideshow':
                if ( ! empty( $bg['slideshowUrls'] ) ) {
                    $lines = explode( "\n", (string) $bg['slideshowUrls'] );
                    $safe  = array_filter( array_map( function ( $line ) {
                        return self::sanitize_url( trim( $line ) );
                    }, $lines ) );
                    $clean['slideshowUrls'] = implode( "\n", $safe );
                }
                if ( ! empty( $bg['slideshowSpeed'] ) ) {
                    $clean['slideshowSpeed'] = self::sanitize_dimension( $bg['slideshowSpeed'] );
                }
                break;
        }

        if ( isset( $bg['opacity'] ) ) {
            $op = (float) $bg['opacity'];
            $clean['opacity'] = (string) max( 0.0, min( 1.0, $op ) );
        }

        return $clean;
    }

    // -------------------------------------------------------------------------
    // Border data sanitizer
    // -------------------------------------------------------------------------

    private static function sanitize_border_data( array $bd ): array {
        if ( empty( $bd ) ) {
            return [];
        }
        $clean = [];

        if ( isset( $bd['borderType'] ) ) {
            $clean['borderType'] = in_array( $bd['borderType'], [ 'none','solid','dashed','dotted','double' ], true )
                ? $bd['borderType'] : 'none';
        }
        if ( ! empty( $bd['borderColor'] ) ) {
            $c = self::sanitize_color( $bd['borderColor'] );
            if ( $c ) $clean['borderColor'] = $c;
        }
        if ( ! empty( $bd['borderWidth'] ) ) {
            $clean['borderWidth'] = self::sanitize_dimension( $bd['borderWidth'] );
        }
        foreach ( [ 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth' ] as $side ) {
            if ( isset( $bd[ $side ] ) && '' !== (string) $bd[ $side ] ) {
                $v = self::sanitize_dimension( (string) $bd[ $side ] );
                if ( '' !== $v ) $clean[ $side ] = $v;
            }
        }
        if ( ! empty( $bd['borderRadius'] ) ) {
            $clean['borderRadius'] = self::sanitize_dimension( $bd['borderRadius'] );
        }
        foreach ( [ 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius' ] as $corner ) {
            if ( isset( $bd[ $corner ] ) && '' !== (string) $bd[ $corner ] ) {
                $v = self::sanitize_dimension( (string) $bd[ $corner ] );
                if ( '' !== $v ) $clean[ $corner ] = $v;
            }
        }
        if ( ! empty( $bd['boxShadow'] ) ) {
            $clean['boxShadow'] = self::sanitize_css_value( $bd['boxShadow'] );
        }

        return $clean;
    }

    // -------------------------------------------------------------------------
    // Primitive sanitizers (public so the renderer can reuse them)
    // -------------------------------------------------------------------------

    /**
     * Sanitize a URL. Dynamic {{tag}} placeholders are preserved as-is.
     * For plain URLs, only http/https/mailto schemes are allowed.
     */
    public static function sanitize_url( string $url ): string {
        $url = trim( $url );
        if ( '' === $url ) {
            return '';
        }
        // Preserve dynamic tag placeholders — resolved server-side at render time.
        if ( str_contains( $url, '{{' ) ) {
            return sanitize_text_field( $url );
        }
        $clean = esc_url_raw( $url );
        // Reject anything that didn't survive esc_url_raw (empty result = bad scheme or malformed)
        if ( '' === $clean ) {
            return '';
        }
        // Whitelist schemes
        $scheme = wp_parse_url( $clean, PHP_URL_SCHEME );
        if ( $scheme && ! in_array( strtolower( $scheme ), [ 'http', 'https', 'mailto' ], true ) ) {
            return '';
        }
        return $clean;
    }

    /**
     * Sanitize a CSS dimension value (e.g. "16px", "1.5", "100%", "auto").
     */
    /**
     * Sanitize a 1-4 value CSS shorthand (padding / margin / border-radius).
     *
     * sanitize_dimension() accepts a SINGLE value, so it returns '' for
     * anything containing a space — silently destroying "10px 14px" or
     * "0 auto" on save. Free-text shorthand controls must use this instead, or
     * every multi-value entry the author types is discarded before the element
     * ever sees it.
     *
     * Each part is validated individually, so no raw string reaches CSS. A
     * bare "0" is permitted because it is valid, and unitless, in a shorthand.
     *
     * @param  string $val Raw value, e.g. "10px 14px".
     * @return string      Validated shorthand, or '' if any part is invalid.
     */
    public static function sanitize_dimension_shorthand( string $val ): string {
        $val = trim( $val );
        if ( '' === $val ) {
            return '';
        }

        $parts = preg_split( '/\s+/', $val );
        if ( ! $parts || count( $parts ) > 4 ) {
            return '';
        }

        $out = [];
        foreach ( $parts as $part ) {
            $clean = self::sanitize_dimension( $part );
            if ( '' === $clean && '0' === $part ) {
                $clean = '0';
            }
            if ( '' === $clean ) {
                return '';
            }
            $out[] = $clean;
        }

        return implode( ' ', $out );
    }

    public static function sanitize_dimension( string $val ): string {
        $val = trim( $val );
        if ( in_array( $val, [ '', 'auto', 'inherit', 'initial', 'unset', 'none', 'normal' ], true ) ) {
            return $val;
        }
        // Number + known CSS unit
        if ( preg_match( '/^-?[\d.]+(%|px|em|rem|vw|vh|dvh|svh|lvh|s|ms|deg|fr|ch|ex)$/', $val ) ) {
            return $val;
        }
        // Unitless number (line-height: 1.5, etc.)
        if ( preg_match( '/^-?[\d.]+$/', $val ) ) {
            return $val;
        }
        // Fluid sizing: clamp()/min()/max() built ONLY from plain numeric
        // lengths. Without this, responsive typography is impossible through
        // node styles — a stripped clamp() leaves the property unset and the
        // browser falls back to its default size.
        //
        // Deliberately strict: comma-separated numeric lengths only. No nested
        // functions, no calc(), no var(), no arithmetic — nothing that could
        // carry arbitrary text into the stylesheet.
        if ( preg_match( '/^(clamp|min|max)\(\s*(-?[\d.]+(?:%|px|em|rem|vw|vh|dvh|svh|lvh|ch|ex)?\s*,\s*){1,4}-?[\d.]+(?:%|px|em|rem|vw|vh|dvh|svh|lvh|ch|ex)?\s*\)$/', $val ) ) {
            // Re-assemble from the matched numbers rather than passing the raw
            // string through, so only validated tokens reach the output.
            if ( preg_match( '/^(clamp|min|max)\((.*)\)$/', $val, $m ) ) {
                $fn    = $m[1];
                $parts = array_map( 'trim', explode( ',', $m[2] ) );
                $safe  = [];
                foreach ( $parts as $part ) {
                    if ( ! preg_match( '/^-?[\d.]+(%|px|em|rem|vw|vh|dvh|svh|lvh|ch|ex)?$/', $part ) ) {
                        return '';
                    }
                    $safe[] = $part;
                }
                return $fn . '(' . implode( ',', $safe ) . ')';
            }
        }
        return '';
    }

    /**
     * Sanitize a CSS color value (hex, rgb/rgba, hsl/hsla, named keywords).
     */
    public static function sanitize_color( string $val ): string {
        $val = trim( $val );
        if ( '' === $val ) {
            return '';
        }

        // Design System token reference: var(--jalpi-color-{id}) or
        // var(--jalpi-color-{id}, <fallback>). The fallback, if present, must
        // itself pass one of the same grammars whitelisted below — no new
        // injection surface is opened by allowing var().
        if ( preg_match( '/^var\(\s*--[a-zA-Z0-9_-]+\s*(,\s*(#[0-9a-fA-F]{3,8}|rgba?\([\d.,\s%]+\)|hsla?\([\d.,\s%]+\)|[a-zA-Z]+)\s*)?\)$/', $val ) ) {
            return $val;
        }

        // Hex (#rgb, #rrggbb, #rrggbbaa)
        if ( preg_match( '/^#[0-9a-fA-F]{3,8}$/', $val ) ) {
            return $val;
        }
        // rgb() / rgba()
        if ( preg_match( '/^rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+(\s*,\s*[\d.]+)?\s*\)$/', $val ) ) {
            return $val;
        }
        // hsl() / hsla()
        if ( preg_match( '/^hsla?\(\s*[\d.]+\s*,\s*[\d.]+%?\s*,\s*[\d.]+%?(\s*,\s*[\d.]+)?\s*\)$/', $val ) ) {
            return $val;
        }
        // Safe named keywords only
        $safe = [
            'transparent','currentcolor','inherit','initial','unset',
            'black','white','red','green','blue','yellow','orange','purple',
            'pink','gray','grey','navy','teal','maroon','olive','aqua',
            'fuchsia','lime','silver','cyan','magenta',
        ];
        if ( in_array( strtolower( $val ), $safe, true ) ) {
            return $val;
        }

        return '';
    }

    /**
     * Strip CSS injection vectors from compound CSS values (box-shadow, border shorthand, gradients).
     */
    public static function sanitize_css_value( string $val ): string {
        $val = wp_strip_all_tags( $val );
        // Strip JS/expression/binding attack vectors
        $dangerous = [
            '/expression\s*\(/i',
            '/-moz-binding\s*:/i',
            '/javascript\s*:/i',
            '/vbscript\s*:/i',
            '/\bdata\s*:/i',
            '/@import\b/i',
            '/<!--.*?-->/s',
        ];
        foreach ( $dangerous as $pattern ) {
            $val = preg_replace( $pattern, '', $val );
        }
        return trim( $val );
    }

    // -------------------------------------------------------------------------
    // Flex alignment helper
    // -------------------------------------------------------------------------

    private static function sanitize_flex_align( string $val ): string {
        return in_array( $val, [ 'flex-start','center','flex-end','stretch','baseline' ], true ) ? $val : '';
    }

    /**
     * Sanitize an icon value — either a legacy string key or an IconValue array.
     * Returns the sanitized value in its original format (string or array).
     *
     * @param  mixed  $raw         The raw icon value from content.
     * @param  string $legacy_default  Fallback key when $raw is a plain string.
     * @return string|array
     */
    /**
     * Demo menu items for design previews.
     *
     * Labels only — no URLs are stored, so a preview link can never navigate
     * out of the sandbox. Accepts plain strings or {label, children} objects.
     * Depth and count are capped so a crafted payload cannot blow the stack or
     * bloat the saved node.
     *
     * @param mixed $raw
     * @return array<int,array>
     */
    public static function sanitize_demo_menu_items( $raw, int $depth = 0 ): array {
        if ( ! is_array( $raw ) || $depth > 5 ) {
            return [];
        }

        $out = [];
        foreach ( $raw as $item ) {
            if ( count( $out ) >= 30 ) {
                break;
            }
            if ( is_string( $item ) ) {
                $item = [ 'label' => $item ];
            }
            if ( ! is_array( $item ) ) {
                continue;
            }

            $label = sanitize_text_field( (string) ( $item['label'] ?? '' ) );
            if ( '' === $label ) {
                continue;
            }

            $clean = [ 'label' => $label ];

            $children = self::sanitize_demo_menu_items( $item['children'] ?? [], $depth + 1 );
            if ( $children ) {
                $clean['children'] = $children;
            }

            $out[] = $clean;
        }

        return $out;
    }

    public static function sanitize_icon_value( $raw, string $legacy_default = 'star' ) {
        if ( is_string( $raw ) ) {
            return sanitize_key( $raw ) ?: $legacy_default;
        }

        if ( ! is_array( $raw ) ) {
            return $legacy_default;
        }

        $type = sanitize_key( $raw['type'] ?? '' );
        $lib  = sanitize_key( $raw['lib']  ?? '' );
        $name = sanitize_text_field( $raw['name'] ?? '' );

        if ( ! in_array( $type, [ 'svg', 'font', 'img' ], true ) ) {
            return $legacy_default;
        }

        $clean = [ 'type' => $type, 'lib' => $lib, 'name' => $name ];

        if ( $type === 'svg' && ! empty( $raw['svg'] ) ) {
            // Allow only known-safe SVG attributes — strips scripts, event handlers, etc.
            $clean['svg'] = wp_kses(
                $raw['svg'],
                [
                    // 'viewbox' lowercase is the one that actually matches:
                    // wp_kses_attr_check() lowercases the attribute name before
                    // looking it up, so a camelCase-only key never matches and
                    // the viewBox is stripped — the icon then loses its
                    // coordinate system and collapses when scaled. Both
                    // spellings are listed so the intent stays obvious.
                    'svg'      => array_fill_keys( [ 'xmlns','viewbox','viewBox','width','height','fill','stroke','stroke-width','stroke-linecap','stroke-linejoin','class','style','aria-hidden' ], true ),
                    'path'     => array_fill_keys( [ 'd','fill','stroke','stroke-width','stroke-linecap','stroke-linejoin','opacity' ], true ),
                    'circle'   => array_fill_keys( [ 'cx','cy','r','fill','stroke','stroke-width' ], true ),
                    'ellipse'  => array_fill_keys( [ 'cx','cy','rx','ry','fill','stroke' ], true ),
                    'rect'     => array_fill_keys( [ 'x','y','width','height','rx','ry','fill','stroke','stroke-width' ], true ),
                    'line'     => array_fill_keys( [ 'x1','y1','x2','y2','stroke','stroke-width' ], true ),
                    'polyline' => array_fill_keys( [ 'points','fill','stroke','stroke-width','stroke-linecap','stroke-linejoin' ], true ),
                    'polygon'  => array_fill_keys( [ 'points','fill','stroke','stroke-width' ], true ),
                    'g'        => array_fill_keys( [ 'fill','stroke','stroke-width','transform','opacity' ], true ),
                    'defs'     => [],
                    'title'    => [],
                ]
            );
        }

        if ( $type === 'font' && ! empty( $raw['cls'] ) ) {
            $clean['cls'] = sanitize_text_field( $raw['cls'] );
        }

        if ( $type === 'img' ) {
            $clean['url'] = ! empty( $raw['url'] ) ? esc_url_raw( $raw['url'] ) : '';
            $clean['alt'] = sanitize_text_field( $raw['alt'] ?? '' );
        }

        return $clean;
    }

    // -------------------------------------------------------------------------
    // KSES allowed HTML for admin HTML blocks
    // -------------------------------------------------------------------------

    private static function kses_html_block(): array {
        // Start from wp_kses_post() allowed set, then extend for structural tags admins need.
        $allowed = wp_kses_allowed_html( 'post' );

        $extensions = [
            'div'        => [ 'class' => true, 'id' => true, 'style' => true ],
            'span'       => [ 'class' => true, 'id' => true, 'style' => true ],
            'section'    => [ 'class' => true, 'id' => true, 'style' => true ],
            'article'    => [ 'class' => true, 'id' => true, 'style' => true ],
            'header'     => [ 'class' => true, 'id' => true, 'style' => true ],
            'footer'     => [ 'class' => true, 'id' => true, 'style' => true ],
            'nav'        => [ 'class' => true, 'id' => true, 'style' => true ],
            'figure'     => [ 'class' => true, 'id' => true ],
            'figcaption' => [ 'class' => true ],
            'table'      => [ 'class' => true, 'id' => true, 'style' => true, 'border' => true, 'cellpadding' => true, 'cellspacing' => true ],
            'thead'      => [ 'class' => true ],
            'tbody'      => [ 'class' => true ],
            'tr'         => [ 'class' => true, 'style' => true ],
            'th'         => [ 'class' => true, 'style' => true, 'colspan' => true, 'rowspan' => true, 'scope' => true ],
            'td'         => [ 'class' => true, 'style' => true, 'colspan' => true, 'rowspan' => true ],
            'iframe'     => [
                'src' => true, 'width' => true, 'height' => true,
                'style' => true, 'class' => true, 'id' => true,
                'frameborder' => true, 'allowfullscreen' => true,
                'loading' => true, 'title' => true,
            ],
            'video'      => [
                'src' => true, 'width' => true, 'height' => true,
                'controls' => true, 'autoplay' => true, 'muted' => true,
                'loop' => true, 'style' => true, 'class' => true,
                'playsinline' => true, 'poster' => true,
            ],
            'audio'      => [
                'src' => true, 'controls' => true, 'autoplay' => true,
                'loop' => true, 'muted' => true,
            ],
            'source'     => [ 'src' => true, 'type' => true ],
            'picture'    => [],
            'svg'        => [
                'class' => true, 'id' => true,
                'width' => true, 'height' => true,
                'viewbox' => true, 'viewBox' => true,
                'fill' => true, 'xmlns' => true,
                'stroke' => true, 'stroke-width' => true,
                'stroke-linecap' => true, 'stroke-linejoin' => true,
                'aria-hidden' => true,
            ],
            'path'       => [ 'd' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'class' => true, 'opacity' => true ],
            'circle'     => [ 'cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true ],
            'rect'       => [ 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true, 'fill' => true, 'stroke' => true ],
            'line'       => [ 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'stroke' => true, 'stroke-width' => true ],
            'polyline'   => [ 'points' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true ],
            'polygon'    => [ 'points' => true, 'fill' => true, 'stroke' => true ],
            'g'          => [ 'class' => true, 'fill' => true, 'stroke' => true, 'transform' => true ],
        ];

        return array_merge( $allowed, $extensions );
    }
}
