<?php
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Frontend reads custom plugin tables and registers local assets with the plugin version when available.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class YATOON_Frontend {
    private static $instance;

    public function __construct() {
        self::$instance = $this;
        add_shortcode('yatoon_booking', array($this, 'booking_shortcode'));
        // Backward-compatible alias used by early Yatoon/Square installs.
        add_shortcode('square_booking', array($this, 'booking_shortcode'));
        add_shortcode('yatoon_catalog', array($this, 'catalog_shortcode'));
        add_shortcode('yatoon_gallery', array($this, 'gallery_shortcode'));
        add_action('template_redirect', array($this, 'protect_personalized_customer_pages'), 0);
        add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_scripts'));
    }

    public static function get_instance() {
        return self::$instance;
    }

    /** Keep verified customer HTML private and start its session before output. */
    public function protect_personalized_customer_pages() {
        if ( is_admin() || wp_doing_ajax() || is_feed() || ! is_singular() ) {
            return;
        }
        $page_id = get_queried_object_id();
        $post = get_post( $page_id );
        $content = $post ? (string) $post->post_content : '';
        $saved_ids = array_filter( array_map( 'absint', array( get_option( 'yatoon_booking_page_id', 0 ), get_option( 'yatoon_portal_page_id', 0 ) ) ) );
        $is_customer_page = in_array( $page_id, $saved_ids, true )
            || has_shortcode( $content, 'yatoon_booking' )
            || has_shortcode( $content, 'square_booking' )
            || has_shortcode( $content, 'yatoon_customer_portal' );
        if ( ! $is_customer_page ) {
            $current_path = untrailingslashit( (string) wp_parse_url( get_permalink( $page_id ), PHP_URL_PATH ) );
            foreach ( array( get_option( 'yatoon_book_new_url', '' ), get_option( 'yatoon_portal_url', '' ) ) as $configured_url ) {
                $configured_path = untrailingslashit( (string) wp_parse_url( $configured_url, PHP_URL_PATH ) );
                if ( $configured_path !== '' && $configured_path === $current_path ) {
                    $is_customer_page = true;
                    break;
                }
            }
        }
        if ( ! $is_customer_page ) {
            return;
        }
        $this->mark_customer_page_dynamic();
        if ( class_exists( 'YATOON_Customer_Portal' ) && YATOON_Customer_Portal::get_instance() ) {
            YATOON_Customer_Portal::get_instance()->bootstrap_session();
        }
    }

    /** Mark a personalized response private for WordPress, LiteSpeed and CDNs. */
    public function mark_customer_page_dynamic() {
        if ( ! defined( 'DONOTCACHEPAGE' ) ) {
            define( 'DONOTCACHEPAGE', true );
        }
        add_filter( 'litespeed_control_set_nocache', '__return_true' );
        if ( ! headers_sent() ) {
            nocache_headers();
            header( 'Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0', true );
            header( 'CDN-Cache-Control: no-store', true );
            header( 'Cloudflare-CDN-Cache-Control: no-store', true );
            header( 'X-LiteSpeed-Cache-Control: no-cache', true );
        }
    }

    /**
     * Preload service options so returning from Add More / Add Guest does not
     * depend on a fresh admin-ajax request before the customer can continue.
     */
    private function get_preloaded_service_options() {
        global $wpdb;

        $services_table = $wpdb->prefix . 'yatoon_services';
        $options_table  = $wpdb->prefix . 'yatoon_service_options';
        $preloaded      = array();

        $options_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $options_table ) );
        $services_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $services_table ) );
        if ( $options_table_exists !== $options_table || $services_table_exists !== $services_table ) {
            return $preloaded;
        }

        $services = $wpdb->get_results( "SELECT id, price, square_id, allow_design_upload FROM {$services_table}" );
        $base_prices = array();
        $square_to_service = array();
        $service_uploads = array();
        foreach ( (array) $services as $service ) {
            $service_id = intval( $service->id );
            $base_prices[ $service_id ] = floatval( $service->price );
            $service_uploads[ $service_id ] = ! empty( $service->allow_design_upload ) ? 1 : 0;
            $preloaded[ $service_id ] = array(
                'options'                     => array(),
                'base_price'                  => $base_prices[ $service_id ],
                'service_allow_design_upload' => $service_uploads[ $service_id ],
            );
            if ( ! empty( $service->square_id ) ) {
                $square_to_service[ (string) $service->square_id ] = $service_id;
            }
        }

        $options = $wpdb->get_results( "SELECT * FROM {$options_table} ORDER BY service_id ASC, sort_order ASC, id ASC" );
        foreach ( (array) $options as $option ) {
            $service_id  = intval( $option->service_id );
            $base_price  = isset( $base_prices[ $service_id ] ) ? $base_prices[ $service_id ] : 0;
            $linked_id   = null;
            $square_id   = isset( $option->square_id ) ? (string) $option->square_id : '';

            if ( $square_id !== '' && isset( $square_to_service[ $square_id ] ) ) {
                $linked_id = intval( $square_to_service[ $square_id ] );
            }

            if ( ! isset( $preloaded[ $service_id ] ) ) {
                $preloaded[ $service_id ] = array(
                    'options'                     => array(),
                    'base_price'                  => $base_price,
                    'service_allow_design_upload' => ! empty( $service_uploads[ $service_id ] ) ? 1 : 0,
                );
            }

            $option_allows_upload = ! empty( $service_uploads[ $service_id ] );
            if ( $linked_id && ! empty( $service_uploads[ $linked_id ] ) ) {
                $option_allows_upload = true;
            }

            $preloaded[ $service_id ]['options'][] = array(
                'id'               => intval( $option->id ),
                'service_id'       => $service_id,
                'square_id'        => $square_id,
                'square_version'   => isset( $option->square_version ) ? $option->square_version : null,
                'name'             => (string) $option->name,
                'description'      => isset( $option->description ) ? (string) $option->description : '',
                'duration_minutes' => intval( $option->duration_minutes ),
                'price_modifier'   => floatval( $option->price_modifier ),
                'is_required'      => intval( $option->is_required ),
                'group_name'       => isset( $option->group_name ) ? $option->group_name : '',
                'badge_label'      => isset( $option->badge_label ) ? (string) $option->badge_label : '',
                'sort_order'       => intval( $option->sort_order ),
                'absolute_price'   => ( isset( $option->group_name ) && $option->group_name === 'Add-on' ) ? floatval( $option->price_modifier ) : ( $base_price + floatval( $option->price_modifier ) ),
                'linked_service_id'=> $linked_id,
                'allow_design_upload' => $option_allows_upload ? 1 : 0,
            );
        }

        return $preloaded;
    }

    /**
     * Preload eligible staff from local tables. Time-slot availability still
     * stays real-time, but the staff chooser should not wait on a network call.
     */
    private function get_preloaded_service_staff() {
        global $wpdb;

        $services_table       = $wpdb->prefix . 'yatoon_services';
        $staff_table          = $wpdb->prefix . 'yatoon_staff';
        $staff_services_table = $wpdb->prefix . 'yatoon_staff_services';
        $preloaded            = array();

        $services_table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $services_table ) );
        $staff_table_exists    = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $staff_table ) );
        if ( $services_table_exists !== $services_table || $staff_table_exists !== $staff_table ) {
            return $preloaded;
        }

        $staff_rows = $wpdb->get_results( "SELECT * FROM {$staff_table} WHERE is_active = 1 AND is_bookable = 1 ORDER BY sort_order ASC, name ASC" );
        $all_staff = array();
        $staff_by_id = array();
        foreach ( (array) $staff_rows as $staff ) {
            $row = array(
                'id'          => intval( $staff->id ),
                'name'        => (string) $staff->name,
                'bio'         => isset( $staff->bio ) ? (string) $staff->bio : '',
                'image_url'   => isset( $staff->image_url ) ? (string) $staff->image_url : '',
                'square_id'   => isset( $staff->square_id ) ? (string) $staff->square_id : '',
                'is_active'   => 1,
                'is_bookable' => 1,
            );
            $all_staff[] = $row;
            $staff_by_id[ intval( $staff->id ) ] = $row;
        }

        $assignments_by_service = array();
        $staff_services_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $staff_services_table ) );
        if ( $staff_services_exists === $staff_services_table ) {
            $assignments = $wpdb->get_results( "SELECT staff_id, service_id FROM {$staff_services_table}" );
            foreach ( (array) $assignments as $assignment ) {
                $service_id = intval( $assignment->service_id );
                $staff_id   = intval( $assignment->staff_id );
                if ( ! isset( $assignments_by_service[ $service_id ] ) ) {
                    $assignments_by_service[ $service_id ] = array();
                }
                if ( isset( $staff_by_id[ $staff_id ] ) ) {
                    $assignments_by_service[ $service_id ][] = $staff_by_id[ $staff_id ];
                }
            }
        }

        $service_ids = $wpdb->get_col( "SELECT id FROM {$services_table}" );
        foreach ( (array) $service_ids as $service_id_raw ) {
            $service_id = intval( $service_id_raw );
            $staff_list = ! empty( $assignments_by_service[ $service_id ] )
                ? array_values( $assignments_by_service[ $service_id ] )
                : $all_staff;

            if ( count( $staff_list ) >= 2 ) {
                array_unshift( $staff_list, array(
                    'id'           => 0,
                    'name'         => 'Any staff',
                    'bio'          => '',
                    'image_url'    => '',
                    'square_id'    => '',
                    'is_any_staff' => true,
                    'is_active'    => 1,
                    'is_bookable'  => 1,
                ) );
            }

            $preloaded[ $service_id ] = array( 'staff' => $staff_list );
        }

        return $preloaded;
    }

    /**
     * Enqueue frontend scripts
     */
    public function enqueue_frontend_scripts() {
        if (!is_singular() && !is_page()) {
            return;
        }

        global $post;
        if ( ! $post ) {
            return;
        }
        $surfaces_in_content = has_shortcode( $post->post_content, 'yatoon_booking' )
            || has_shortcode( $post->post_content, 'sbs_customer_portal' )
            || has_shortcode( $post->post_content, 'yatoon_customer_portal' )
            || ( function_exists( 'has_block' ) && (
                has_block( 'yatoon/booking', $post )
                || has_block( 'yatoon/customer-portal', $post )
            ) );
        if ( ! $surfaces_in_content ) {
            return;
        }

        $this->render_frontend_assets();
    }

    /**
     * Loads the booking form's CSS/JS. Split out from enqueue_frontend_scripts()
     * so it can also be called directly from booking_shortcode() and from the
     * customer portal's shortcode -- has_shortcode()/has_block() above can only
     * detect the literal shortcode text or a Gutenberg block comment in
     * post_content, but this same output can also come from an Elementor
     * widget (stored separately, not in post_content at all) or from
     * do_shortcode() called from a template. Calling this at the point the
     * shortcode actually renders guarantees the assets load regardless of how
     * it got there. Idempotent, so calling it more than once is harmless.
     */
    public function render_frontend_assets() {
        static $already_rendered = false;
        if ( $already_rendered ) {
            return;
        }
        $already_rendered = true;

        // Keep component files readable under SCRIPT_DEBUG. Production uses a
        // single cascade-preserving CSS bundle and a single interaction bundle.
        $is_debug = defined('SCRIPT_DEBUG') && SCRIPT_DEBUG;
        $min = $is_debug ? '' : '.min';
        $min_css = file_exists( SBS_PLUGIN_DIR . 'assets/css/frontend.min.css' ) ? $min : '';
        $min_js  = file_exists( SBS_PLUGIN_DIR . 'assets/js/frontend.min.js' ) ? $min : '';
        $category_min_js  = file_exists( SBS_PLUGIN_DIR . 'assets/js/yatoon-category-bar-423-path16.min.js' ) ? $min : '';
        $category_min_css = file_exists( SBS_PLUGIN_DIR . 'assets/css/yatoon-category-bar-423-path16.min.css' ) ? $min : '';

        $frontend_css_file = SBS_PLUGIN_DIR . 'assets/css/frontend' . $min_css . '.css';
        $frontend_js_file  = SBS_PLUGIN_DIR . 'assets/js/frontend' . $min_js . '.js';
        $bundle_css_suffix = is_rtl() ? '-rtl' : '';
        $bundle_css_file   = SBS_PLUGIN_DIR . 'assets/css/yatoon-booking-bundle.min' . $bundle_css_suffix . '.css';
        $category_bar_js_file  = SBS_PLUGIN_DIR . 'assets/js/yatoon-category-bar-423-path16' . $category_min_js . '.js';
        $category_bar_css_file = SBS_PLUGIN_DIR . 'assets/css/yatoon-category-bar-423-path16' . $category_min_css . '.css';
        $apple_polish_file = SBS_PLUGIN_DIR . 'assets/css/apple-polish' . YATOON_ASSET_SUFFIX . '.css';
        $ui_polish_file    = SBS_PLUGIN_DIR . 'assets/css/yatoon-ui-polish' . YATOON_ASSET_SUFFIX . '.css';
        $frontend_css_ver  = file_exists($frontend_css_file) ? SBS_VERSION . '.' . filemtime($frontend_css_file) : SBS_VERSION;
        $bundle_css_ver    = file_exists($bundle_css_file) ? SBS_VERSION . '.' . filemtime($bundle_css_file) : SBS_VERSION;
        $frontend_js_ver   = file_exists($frontend_js_file) ? SBS_VERSION . '.' . filemtime($frontend_js_file) : SBS_VERSION;
        $category_bar_js_ver  = file_exists($category_bar_js_file) ? SBS_VERSION . '.423.' . filemtime($category_bar_js_file) : SBS_VERSION . '.423';
        $category_bar_css_ver = file_exists($category_bar_css_file) ? SBS_VERSION . '.423.' . filemtime($category_bar_css_file) : SBS_VERSION . '.423';
        $apple_polish_ver  = file_exists($apple_polish_file) ? SBS_VERSION . '.' . filemtime($apple_polish_file) : SBS_VERSION;
        $ui_polish_ver     = file_exists($ui_polish_file) ? SBS_VERSION . '.' . filemtime($ui_polish_file) : SBS_VERSION;

        $final_style_handle = 'yatoon-motion-polish';
        if ( ! $is_debug && file_exists( $bundle_css_file ) ) {
            // Every layout-affecting stylesheet must be present before first
            // paint. Loading visual "enhancements" after pointer intent caused
            // the booking form to render wide, then visibly shrink seconds later.
            wp_enqueue_style( 'sbs-frontend-css', SBS_PLUGIN_URL . 'assets/css/yatoon-booking-bundle.min' . $bundle_css_suffix . '.css', array(), $bundle_css_ver );
            $final_style_handle = 'sbs-frontend-css';
        } else {
            wp_enqueue_style('sbs-frontend-css', SBS_PLUGIN_URL . 'assets/css/frontend' . $min_css . '.css', array(), $frontend_css_ver);
            wp_enqueue_style('yatoon-apple-polish', SBS_PLUGIN_URL . 'assets/css/apple-polish' . YATOON_ASSET_SUFFIX . '.css', array('sbs-frontend-css'), $apple_polish_ver);
            wp_enqueue_style('yatoon-ui-polish', SBS_PLUGIN_URL . 'assets/css/yatoon-ui-polish' . YATOON_ASSET_SUFFIX . '.css', array('yatoon-apple-polish'), $ui_polish_ver);
            wp_enqueue_style('yatoon-design-system', SBS_PLUGIN_URL . 'assets/css/yatoon-design-system' . YATOON_ASSET_SUFFIX . '.css', array('yatoon-ui-polish'), SBS_VERSION);
            $motion_polish_ver = ( file_exists( SBS_PLUGIN_DIR . 'assets/css/yatoon-motion-polish' . $min . '.css' ) ) ? filemtime( SBS_PLUGIN_DIR . 'assets/css/yatoon-motion-polish' . $min . '.css' ) : YATOON_VERSION;
            wp_enqueue_style('yatoon-motion-polish', SBS_PLUGIN_URL . 'assets/css/yatoon-motion-polish' . $min . '.css', array('yatoon-design-system'), $motion_polish_ver);
            wp_enqueue_style('yatoon-category-bar-423', SBS_PLUGIN_URL . 'assets/css/yatoon-category-bar-423-path16' . $category_min_css . '.css', array('yatoon-motion-polish'), $category_bar_css_ver);
        }
        // Attach runtime fixes to the actual final stylesheet in each path so
        // no later component can undo them.
        wp_add_inline_style( $final_style_handle, yatoon_apple_polish_inline_css() );
        // Keep the Google control compact even when a theme/page builder applies
        // broad SVG rules. This is attached to the stylesheet that is actually
        // enqueued in both bundled production and SCRIPT_DEBUG modes.
        // 4.6.1 V2: the static override layer moved out of this file and into
        // assets/css/yatoon-booking-final.css so browsers can cache it. It is
        // enqueued last, and depends on the final stylesheet handle, so it still
        // wins every cascade fight it was written to win.
        // Same RTL convention the bundle uses: "<name>.min-rtl.css".
        $final_css_rtl  = is_rtl() ? '-rtl' : '';
        $final_css_file = SBS_PLUGIN_DIR . 'assets/css/yatoon-booking-final' . YATOON_ASSET_SUFFIX . $final_css_rtl . '.css';
        if ( ! file_exists( $final_css_file ) ) {
            $final_css_file = SBS_PLUGIN_DIR . 'assets/css/yatoon-booking-final' . $final_css_rtl . '.css';
        }
        if ( ! file_exists( $final_css_file ) ) {
            $final_css_file = SBS_PLUGIN_DIR . 'assets/css/yatoon-booking-final.css';
        }
        $final_css_rel  = str_replace( SBS_PLUGIN_DIR, '', $final_css_file );
        wp_enqueue_style(
            'yatoon-booking-final',
            SBS_PLUGIN_URL . $final_css_rel,
            array( $final_style_handle ),
            SBS_VERSION . '.' . ( file_exists( $final_css_file ) ? filemtime( $final_css_file ) : '0' )
        );
        $final_style_handle = 'yatoon-booking-final';

        // 4.6.1: card on file. Stripe.js has to come from Stripe's own domain --
        // PCI rules forbid self-hosting it, and a stale local copy would be a
        // security problem rather than a performance win. Both files load only
        // when the salon has actually switched the feature on.
        if ( class_exists( 'YATOON_Card_On_File' ) && YATOON_Card_On_File::is_enabled() ) {
            // Fall back to the unminified file rather than 404ing if a build
            // step was skipped -- a missing card script means the customer
            // cannot save a card at all.
            $cof_suffix = ( $min_js && file_exists( SBS_PLUGIN_DIR . 'assets/js/yatoon-card-on-file' . $min_js . '.js' ) ) ? $min_js : '';
            $cof_file   = SBS_PLUGIN_DIR . 'assets/js/yatoon-card-on-file' . $cof_suffix . '.js';
            $cof_ver    = file_exists( $cof_file ) ? SBS_VERSION . '.' . filemtime( $cof_file ) : SBS_VERSION;
            wp_enqueue_script( 'stripe-js', 'https://js.stripe.com/v3/', array(), null, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
            wp_enqueue_script( 'yatoon-card-on-file', SBS_PLUGIN_URL . 'assets/js/yatoon-card-on-file' . $cof_suffix . '.js', array( 'jquery', 'stripe-js' ), $cof_ver, true );
            wp_localize_script( 'yatoon-card-on-file', 'yatoonCof', array(
                'ajaxUrl'        => admin_url( 'admin-ajax.php' ),
                'nonce'          => wp_create_nonce( 'yatoon_frontend_nonce' ),
                'publishableKey' => YATOON_Card_On_File::publishable_key(),
                'required'       => 'required' === YATOON_Card_On_File::capture_mode() ? 1 : 0,
            ) );
        }

        if ( ! $is_debug && file_exists( SBS_PLUGIN_DIR . 'assets/js/yatoon-booking-bundle.min.js' ) ) {
            $script_bundle = SBS_PLUGIN_DIR . 'assets/js/yatoon-booking-bundle.min.js';
            wp_enqueue_script( 'sbs-frontend-js', SBS_PLUGIN_URL . 'assets/js/yatoon-booking-bundle.min.js', array('jquery'), SBS_VERSION . '.' . filemtime( $script_bundle ), true );
        } else {
            wp_enqueue_script('yatoon-category-bar-423', SBS_PLUGIN_URL . 'assets/js/yatoon-category-bar-423-path16' . $category_min_js . '.js', array('jquery'), $category_bar_js_ver, true);
            wp_enqueue_script('sbs-frontend-js', SBS_PLUGIN_URL . 'assets/js/frontend' . $min_js . '.js', array('jquery', 'yatoon-category-bar-423'), $frontend_js_ver, true);
        }
        // Same handle in both branches, so one call covers the bundled and the
        // SCRIPT_DEBUG split path. Without this the booking flow -- which draws
        // essentially all of its copy from JS -- stayed English in every locale.
        yatoon_set_script_i18n( 'sbs-frontend-js' );
        // Cloudflare Turnstile's remote script is not enqueued in the free
        // WordPress.org-safe package; remote offloaded scripts fail plugin checks.

        // External phone assets are intentionally not loaded in this build.
        // Inject brand CSS variables
        $primary        = get_option('yatoon_primary_color',      '#ec4899');
        $price_color    = get_option('yatoon_price_color',        '#ec4899');
        $banner_bg      = get_option('yatoon_banner_bg_color',    '#fdf2f8');
        $banner_text    = get_option('yatoon_banner_text_color',  '#9d174d');
        $banner_btn     = get_option('yatoon_banner_btn_color',   '#ec4899');
        $textus_bg      = get_option('yatoon_textus_bg_color',    '#ec4899');
        $textus_text    = get_option('yatoon_textus_text_color',  '#ffffff');
        $secondary      = get_option('yatoon_secondary_color',    '#f9a8d4');
        $text           = get_option('yatoon_text_color',         '#1a1a1a');
        $accent         = get_option('yatoon_accent_color',       '#000000');
        $plugin_bg      = sanitize_hex_color(get_option('yatoon_plugin_page_bg_color', '#f5f5f7')) ?: '#f5f5f7';
        $plugin_card_bg = sanitize_hex_color(get_option('yatoon_plugin_card_bg_color', '#ffffff')) ?: '#ffffff';
        $readmore_color = get_option('sbs_readmore_color', get_option('yatoon_readmore_color', '#16a34a'));
        // NOTE: the Shape & Typography admin UI (settings.php) saves these
        // under sbs_button_radius/sbs_card_radius/sbs_font_family - the
        // yatoon_* names were legacy/unused option names nothing wrote to,
        // same class of mismatch already worked around below for
        // sbs_main_title_size.
        $btn_r          = intval(get_option('sbs_button_radius', get_option('yatoon_button_radius', 8)));
        $card_r         = intval(get_option('sbs_card_radius',   get_option('yatoon_card_radius',   12)));
        $font           = get_option('sbs_font_family', get_option('yatoon_font_family', 'system'));
        $heading_font   = get_option('sbs_heading_font', get_option('yatoon_heading_font', 'bebas'));
        $heading_color  = get_option('sbs_heading_color', get_option('yatoon_heading_color', '#ec4899'));
        $heading_size   = intval(get_option('sbs_heading_size', get_option('yatoon_heading_size', 32)));
        $main_title_sz  = intval(get_option('sbs_main_title_size', get_option('yatoon_main_title_size', 34)));
        $cat_tab_sz     = (float) get_option('sbs_cat_tab_size', get_option('yatoon_cat_tab_size', 15));
        $cat_tab_sz     = max(8, min(28, $cat_tab_sz));
        $cat_tab_mobile_sz = max(8, min(12, $cat_tab_sz));
        $booking_template = sanitize_key((string) get_option('yatoon_booking_template', 'beauty'));
        $template_palettes = array(
            'beauty' => array('#db2777', '#be185d', '#fce7f3', '#1a1a1a', '#db2777', '#db2777'),
            'clean'  => array('#2563eb', '#1d4ed8', '#eff6ff', '#111827', '#2563eb', '#111827'),
            'luxury' => array('#92722a', '#78591e', '#fdf8ee', '#1a1109', '#92722a', '#92722a'),
        );
        if (isset($template_palettes[ $booking_template ])) {
            list($primary, $secondary, $banner_bg, $text, $price_color, $heading_color) = $template_palettes[ $booking_template ];
            $banner_text    = $booking_template === 'luxury' ? '#5c4414' : ( $booking_template === 'clean' ? '#1e40af' : '#9d174d' );
            $banner_btn     = $primary;
            $textus_bg      = $primary;
            $readmore_color = get_option('sbs_readmore_color', get_option('yatoon_readmore_color', $primary));
            $heading_color  = get_option('sbs_heading_color', get_option('yatoon_heading_color', $heading_color));
        }

        // -- Button fonts (per-button, falls back to shared sbs_btn_font) --------
        $btn_font_key        = get_option('yatoon_btn_font',          'system');
        $add_btn_font_key    = get_option('yatoon_add_btn_font',      $btn_font_key);
        $back_btn_font_key   = get_option('yatoon_back_btn_font',     $btn_font_key);
        $cont_btn_font_key   = get_option('sbs_continue_btn_font', $btn_font_key);
        $more_btn_font_key   = get_option('sbs_addmore_btn_font',  $btn_font_key);
        $more2_btn_font_key  = get_option('sbs_addmore2_btn_font', $btn_font_key);
        $more3_btn_font_key  = get_option('sbs_addmore3_btn_font', $btn_font_key);
        $conf_btn_font_key   = get_option('sbs_confirm_btn_font',  $btn_font_key);

        // -- Per-button settings ----------------------------------------------
        $add_btn_label    = get_option('yatoon_add_btn_label',          'Add');
        $add_btn_size     = intval(get_option('yatoon_add_btn_size',    14));
        $add_btn_color    = get_option('yatoon_add_btn_text_color',     '#ffffff');
        $add_btn_bg       = get_option('yatoon_add_btn_bg_color',       '#ec4899');

        $back_btn_label   = get_option('yatoon_back_btn_label',         'Back');
        $back_btn_size    = intval(get_option('yatoon_back_btn_size',   14));
        $back_btn_color   = get_option('yatoon_back_btn_text_color',    '#374151');
        $back_btn_bg      = get_option('yatoon_back_btn_bg_color',      '#f3f4f6');

        $cont_btn_label   = get_option('sbs_continue_btn_label',     'Continue');
        $cont_btn_size    = intval(get_option('sbs_continue_btn_size', 15));
        $cont_btn_color   = get_option('sbs_continue_btn_text_color', '#ffffff');
        $cont_btn_bg      = get_option('sbs_continue_btn_bg_color',   '#008a00');

        $more_btn_label   = get_option('sbs_addmore_btn_label',      'Add Another Appointment');
        $more_btn_size    = intval(get_option('sbs_addmore_btn_size', 14));
        $more_btn_color   = get_option('sbs_addmore_btn_text_color',  '#ffffff');
        $more_btn_bg      = get_option('sbs_addmore_btn_bg_color',    '#ec4899');

        $more2_btn_label  = get_option('sbs_addmore2_btn_label',      '+ Add More Services');
        $more2_btn_size   = intval(get_option('sbs_addmore2_btn_size', 16));
        $more2_btn_color  = get_option('sbs_addmore2_btn_text_color',  '#ffffff');
        $more2_btn_bg     = get_option('sbs_addmore2_btn_bg_color',    '#e00000');

        $more3_btn_label  = get_option('sbs_addmore3_btn_label',      '+ Add More Services');
        $more3_btn_size   = intval(get_option('sbs_addmore3_btn_size', 13));
        $more3_btn_color  = get_option('sbs_addmore3_btn_text_color',  '#ffffff');
        $more3_btn_bg     = get_option('sbs_addmore3_btn_bg_color',    '#e00000');

        $add_for_person_btn_label  = get_option('sbs_add_for_person_btn_label',      '+ Add Service for');
        if (trim($add_for_person_btn_label) === '+ Add Service for') {
            $add_for_person_btn_label = 'Add service for';
        }
        $add_for_person_btn_size   = intval(get_option('sbs_add_for_person_btn_size', 12));
        $add_for_person_btn_color  = get_option('sbs_add_for_person_btn_text_color',  '#ffffff');
        $add_for_person_btn_bg     = get_option('sbs_add_for_person_btn_bg_color',    '#16a34a');
        $add_for_person_btn_radius = intval(get_option('sbs_add_for_person_btn_radius', 20));

        $add_guest_btn_label  = get_option('sbs_add_guest_btn_label',      '+ Add Guest Together');
        if (in_array(trim($add_guest_btn_label), array('+ Add Guest', '+ Add Guest Together'), true)) {
            $add_guest_btn_label = 'Add guest together';
        }
        $add_guest_btn_size   = intval(get_option('sbs_add_guest_btn_size', 12));
        $add_guest_btn_color  = get_option('sbs_add_guest_btn_text_color',  '#16a34a');
        $add_guest_btn_bg     = get_option('sbs_add_guest_btn_bg_color',    '#f0fdf4');
        $add_guest_btn_radius = intval(get_option('sbs_add_guest_btn_radius', 20));
        $max_guests_together  = max(0, min(10, intval(get_option('yatoon_max_guests_together', 2))));

        $conf_btn_label   = get_option('sbs_confirm_btn_label',      'Confirm All Bookings');
        $conf_btn_size    = intval(get_option('sbs_confirm_btn_size', 15));
        $conf_btn_color   = get_option('sbs_confirm_btn_text_color',  '#ffffff');
        $conf_btn_bg      = get_option('sbs_confirm_btn_bg_color',    '#16a34a');

        $conf_single_btn_label = get_option('sbs_confirm_single_btn_label', 'Confirm Booking');
        $conf_single_btn_size  = intval(get_option('sbs_confirm_single_btn_size', 15));
        $conf_single_btn_color = get_option('sbs_confirm_single_btn_text_color',  '#ffffff');
        $conf_single_btn_bg    = get_option('sbs_confirm_single_btn_bg_color',    '#16a34a');

        // Per-button border radius
        $add_btn_radius          = intval(get_option('yatoon_add_btn_radius',          20));
        $back_btn_radius         = intval(get_option('yatoon_back_btn_radius',         20));
        $cont_btn_radius         = intval(get_option('sbs_continue_btn_radius',     20));
        $more_btn_radius         = intval(get_option('sbs_addmore_btn_radius',      20));
        $more2_btn_radius        = intval(get_option('sbs_addmore2_btn_radius',     20));
        $more3_btn_radius        = intval(get_option('sbs_addmore3_btn_radius',     20));
        $conf_btn_radius         = intval(get_option('sbs_confirm_btn_radius',      20));
        $conf_single_btn_radius  = intval(get_option('sbs_confirm_single_btn_radius', 20));

        // -- Google Fonts map (body + heading + button) -----------------------
        $all_gfonts = array(
            'system'       => array(null,                null),
            'inter'        => array('Inter',             'Inter:wght@400;500;600;700'),
            'poppins'      => array('Poppins',           'Poppins:wght@400;500;600;700'),
            'roboto'       => array('Roboto',            'Roboto:wght@400;500;700'),
            'montserrat'   => array('Montserrat',        'Montserrat:wght@400;500;600;700'),
            'lato'         => array('Lato',              'Lato:wght@400;700'),
            'playfair'     => array('Playfair Display',  'Playfair+Display:wght@400;700'),
            'bebas'        => array('Bebas Neue',        'Bebas+Neue'),
            'anton'        => array('Anton',             'Anton'),
            'black_han'    => array('Black Han Sans',    'Black+Han+Sans'),
            'boogaloo'     => array('Boogaloo',          'Boogaloo'),
            'archivo_black'=> array('Archivo Black',     'Archivo+Black'),
            'oswald'       => array('Oswald',            'Oswald:wght@400;600;700'),
            'exo'          => array('Exo 2',             'Exo+2:wght@400;700;900'),
            'playfair_h'   => array('Playfair Display',  'Playfair+Display:wght@700;900'),
            'nunito'       => array('Nunito',            'Nunito:wght@400;600;700'),
            'raleway'      => array('Raleway',           'Raleway:wght@400;600;700'),
            'ubuntu'       => array('Ubuntu',            'Ubuntu:wght@400;500;700'),
        );

        // Body font. WordPress.org does not permit a plugin to offload static
        // assets to a remote font CDN. Named families remain useful when the
        // active theme (or a site-local stylesheet) already provides them;
        // otherwise the system stack is the deterministic fallback.
        $font_stack  = 'system-ui, -apple-system, sans-serif';
        $font_import = '';
        if (isset($all_gfonts[ $font ]) && $all_gfonts[ $font ][0]) {
            $font_stack = "'" . $all_gfonts[ $font ][0] . "', system-ui, -apple-system, sans-serif";
        }

        // Heading font
        $hfont_stack  = 'system-ui, -apple-system, sans-serif';
        $hfont_import = '';
        if (isset($all_gfonts[ $heading_font ])) {
            if ($all_gfonts[ $heading_font ][0]) {
                $hfont_stack = "'" . $all_gfonts[ $heading_font ][0] . "', system-ui, -apple-system, sans-serif";
            }
        }

        // -- Resolve font stack for each button separately -------------------
        function yatoon_resolve_font( $key, &$all_gfonts ) {
            if (isset($all_gfonts[ $key ]) && $all_gfonts[ $key ][0]) {
                return array(
                    "'" . $all_gfonts[ $key ][0] . "', system-ui, -apple-system, sans-serif",
                    '',
                );
            }
            return array('inherit', '');
        }
        list($add_btn_font_stack,  $add_btn_font_import)  = yatoon_resolve_font($add_btn_font_key,  $all_gfonts);
        list($back_btn_font_stack, $back_btn_font_import) = yatoon_resolve_font($back_btn_font_key, $all_gfonts);
        list($cont_btn_font_stack, $cont_btn_font_import) = yatoon_resolve_font($cont_btn_font_key, $all_gfonts);
        list($more_btn_font_stack, $more_btn_font_import) = yatoon_resolve_font($more_btn_font_key, $all_gfonts);
        list($more2_btn_font_stack, $more2_btn_font_import) = yatoon_resolve_font($more2_btn_font_key, $all_gfonts);
        list($more3_btn_font_stack, $more3_btn_font_import) = yatoon_resolve_font($more3_btn_font_key, $all_gfonts);
        list($conf_btn_font_stack, $conf_btn_font_import) = yatoon_resolve_font($conf_btn_font_key, $all_gfonts);

        // Remote font imports are intentionally unsupported in distributable
        // builds; keep the existing composition variable empty.
        $all_imports = '';

        $custom_css = "
        {$all_imports}
        :root {
            /* --primary-* mirrors --sbs-primary/secondary/light below so the
               base .sbs-booking-container styles (and anything not wrapped
               in a .yatoon-template-beauty/clean/luxury class) always follow
               the merchant's actual configured color instead of the static
               CSS default. The .yatoon-template-* classes further down still
               set their own --primary-* too (higher specificity, so they
               keep winning for the three named templates) - this line is
               what makes the *base* value correct for everything else. The
               structural --apple-ink/--apple-surface neutrals are untouched
               and stay black/white as designed. */
            --primary-color:       {$primary};
            --primary-hover:       {$secondary};
            --primary-light:       {$banner_bg};
            --primary-bg:          color-mix(in srgb, {$primary} 4%, transparent);
            --sbs-primary:          {$primary};
            --sbs-secondary:        {$secondary};
            --sbs-text:             {$text};
            --sbs-accent:           {$accent};
            --sbs-plugin-bg:        {$plugin_bg};
            --sbs-plugin-card-bg:   {$plugin_card_bg};
            --sbs-readmore:         {$readmore_color};
            --sbs-btn-radius:       {$btn_r}px;
            --sbs-card-radius:      {$card_r}px;
            --sbs-font:             {$font_stack};
            --sbs-hfont:            {$hfont_stack};
            --sbs-heading-color:    {$heading_color};
            --sbs-heading-size:     {$heading_size}px;
            --sbs-main-title-sz:    {$main_title_sz}px;
            --sbs-cat-tab-sz:       clamp(8px, calc(-7.35px + 4.5vw), {$cat_tab_sz}px);
            --sbs-price-color:      {$price_color};
            --sbs-banner-bg:        {$banner_bg};
            --sbs-banner-text:      {$banner_text};
            --sbs-banner-btn:       {$banner_btn};
            --sbs-textus-bg:        {$textus_bg};
            --sbs-textus-text:      {$textus_text};
        }
        .sbs-booking-container { font-family: var(--sbs-font); color: var(--sbs-text); }
        /* Price color */
        .sbs-service-price, .sbs-option-price { color: var(--sbs-price-color) !important; }
        /* Text Us button */
        #sbs-text-us-btn { background: var(--sbs-textus-bg) !important; color: var(--sbs-textus-text) !important; box-shadow: 0 4px 16px color-mix(in srgb, var(--sbs-textus-bg) 40%, transparent) !important; }
        .sbs-service-item.selected, .sbs-option-item.selected { border-color: var(--sbs-primary); }
        .sbs-prog-step.active { background: var(--sbs-primary); }
        .sbs-cart-bar { border-top: 3px solid var(--sbs-primary); }

        /* Category section titles */
        .sbs-category-title {
            font-family: var(--sbs-hfont) !important;
            color: var(--sbs-heading-color) !important;
            font-size: var(--sbs-heading-size) !important;
            font-weight: 900 !important;
            letter-spacing: .03em;
            text-shadow: 2px 2px 0px rgba(0,0,0,.12), 3px 3px 6px rgba(0,0,0,.10);
        }
        /* Book Your Appointment main title */
        .sbs-page-title, #sbs-main-title, #sbs-add-more-title {
            font-size: var(--sbs-main-title-sz) !important;
            font-weight: 800 !important;
        }
        /* Category tabs */
        .sbs-cat-tab {
            font-size: var(--sbs-cat-tab-sz);
            font-weight: 600 !important;
        }
        @media (max-width: 782px) {
            .sbs-category-tabs-wrap,
            body.sbs-booking-immersive .sbs-category-tabs-wrap {
                position: static !important;
                top: auto !important;
                z-index: auto !important;
                margin: 0 0 4px !important;
                padding: 0 0 4px !important;
                overflow: visible !important;
                background: transparent !important;
                backdrop-filter: none !important;
            }
            .sbs-category-tabs,
            body.sbs-booking-immersive .sbs-category-tabs {
                display: flex !important;
                gap: 7px !important;
                max-width: 100% !important;
                padding: 0 2px 4px !important;
                overflow-x: auto !important;
                overflow-y: hidden !important;
                scroll-padding-left: 2px !important;
                -webkit-overflow-scrolling: touch;
            }
            .sbs-cat-tab,
            .sbs-category-tab,
            body.sbs-booking-immersive .sbs-cat-tab {
                flex: 0 0 auto !important;
                min-height: 36px !important;
                max-width: 142px !important;
                padding: 8px 10px !important;
                font-size: {$cat_tab_mobile_sz}px !important;
                line-height: 1.05 !important;
                letter-spacing: 0 !important;
                white-space: nowrap !important;
                overflow: hidden !important;
                text-overflow: ellipsis !important;
            }
            .sbs-cat-tab.active,
            .sbs-category-tab.active,
            body.sbs-booking-immersive .sbs-cat-tab.active {
                max-width: 156px !important;
            }
            .sbs-cat-more-btn,
            #sbs-cat-more-btn,
            body.sbs-booking-immersive #sbs-cat-more-btn {
                max-width: 94px !important;
            }
        }
        ";

        // Per-button dedicated overrides (Button Labels, Fonts & Colours admin
        // panel) - split out from $custom_css on purpose. Several later-loaded
        // stylesheets (apple-polish.css, yatoon-ui-polish.css) each carry their
        // own global "button skin" rules with !important on background/color/
        // border-radius for these same classes, tied to the separate "Primary
        // Action Buttons" setting or older skin variables. Because $custom_css
        // is attached to 'sbs-frontend-css' (loads before those files), the
        // dedicated per-button settings below were always losing to whichever
        // global skin rule happened to load last - not a caching issue, a
        // real specificity/order conflict. Attaching this block to the
        // last-loaded stylesheet instead ('yatoon-motion-polish') makes these
        // dedicated settings the true final word, without having to hunt down
        // and edit every competing rule in those other files.
        // 4.6.2: the per-button settings below were losing to the skin
        // stylesheets, and NOT because of load order. The booking container
        // always carries a .yatoon-template-* class (default "beauty"), and the
        // bundle ships rules like
        //     .yatoon-template-beauty .sbs-book-btn { background:...!important }
        // at specificity (0,2,0) -- plus (0,3,0) variants for :hover and
        // (0,3,0)/(0,4,0) for .sbs-btn-next/.sbs-btn-submit through
        // :not(:disabled):not(.is-disabled). The old override was a bare
        // .sbs-book-btn at (0,1,0). Between two !important declarations the
        // higher specificity wins regardless of which stylesheet was enqueued
        // last, so moving this block onto the final handle (4.6.1) could never
        // have fixed it: a merchant could set the ADD colour, see it in the
        // admin live preview, and still get the template's pink gradient on the
        // booking page. yatoon_btn_sel() repeats the class so an explicit
        // merchant setting outranks every skin, template and location rule.
        $add_btn_shadow        = yatoon_btn_glow( $add_btn_bg );
        $cont_btn_shadow       = yatoon_btn_glow( $cont_btn_bg );
        $conf_btn_shadow       = yatoon_btn_glow( $conf_btn_bg );
        $conf_single_btn_shadow = yatoon_btn_glow( $conf_single_btn_bg );

        $sel_add         = yatoon_btn_sel( '.sbs-book-btn' );
        $sel_back        = yatoon_btn_sel( '.sbs-btn-back' );
        $sel_next        = yatoon_btn_sel( '.sbs-btn-next' );
        $sel_more        = yatoon_btn_sel( '.sbs-btn-add-more' );
        $sel_more2       = yatoon_btn_sel( '.sbs-btn-add-more-calendar', '.sbs-navigation-buttons' );
        $sel_more3       = yatoon_btn_sel( '.sbs-btn-add-more-banner', '#sbs-pending-services-banner' );
        $sel_for_person  = yatoon_btn_sel( '.sbs-add-service-for-person', '#sbs-pending-services-banner' );
        $sel_add_guest   = yatoon_btn_sel( '#sbs-banner-add-guest' );
        $sel_submit      = yatoon_btn_sel( '.sbs-btn-submit' );
        $sel_submit_one  = yatoon_btn_sel( '.sbs-btn-submit.sbs-btn-single' );

        $btn_overrides_css = "
        /* ADD / service button */
        {$sel_add} {
            background: {$add_btn_bg} !important;
            border-color: {$add_btn_bg} !important;
            {$add_btn_shadow}
            color: {$add_btn_color} !important;
            font-size: {$add_btn_size}px !important;
            font-family: {$add_btn_font_stack} !important;
            border-radius: {$add_btn_radius}px !important;
        }
        {$sel_add}:hover {
            background: {$add_btn_bg} !important;
            border-color: {$add_btn_bg} !important;
            color: {$add_btn_color} !important;
        }
        /* Back button */
        {$sel_back} {
            background: {$back_btn_bg} !important;
            color: {$back_btn_color} !important;
            font-size: {$back_btn_size}px !important;
            font-family: {$back_btn_font_stack} !important;
            border-radius: {$back_btn_radius}px !important;
        }
        {$sel_back}:hover {
            background: {$back_btn_bg} !important;
            color: {$back_btn_color} !important;
        }
        /* Continue button */
        {$sel_next} {
            background: {$cont_btn_bg} !important;
            border-color: {$cont_btn_bg} !important;
            {$cont_btn_shadow}
            color: {$cont_btn_color} !important;
            font-size: {$cont_btn_size}px !important;
            font-family: {$cont_btn_font_stack} !important;
            border-radius: {$cont_btn_radius}px !important;
        }
        {$sel_next}:hover {
            background: {$cont_btn_bg} !important;
            border-color: {$cont_btn_bg} !important;
            color: {$cont_btn_color} !important;
        }
        /* Add More Services button (Step 5 Review page) */
        {$sel_more} {
            background: {$more_btn_bg} !important;
            color: {$more_btn_color} !important;
            font-size: {$more_btn_size}px !important;
            font-family: {$more_btn_font_stack} !important;
            border-radius: {$more_btn_radius}px !important;
            flex: 0 0 auto !important;
        }
        {$sel_more}:hover {
            background: {$more_btn_bg} !important;
            color: {$more_btn_color} !important;
        }
        /* Add More Services - calendar bottom nav bar (next to Back)*/
        {$sel_more2} {
            background: {$more2_btn_bg} !important;
            color: {$more2_btn_color} !important;
            font-size: {$more2_btn_size}px !important;
            font-family: {$more2_btn_font_stack} !important;
            border-radius: {$more2_btn_radius}px !important;
            flex: 1 !important;
        }
        {$sel_more2}:hover {
            background: {$more2_btn_bg} !important;
            color: {$more2_btn_color} !important;
        }
        /* Add More Services - calendar top green banner right button */
        {$sel_more3} {
            background: {$more3_btn_bg} !important;
            color: {$more3_btn_color} !important;
            font-size: {$more3_btn_size}px !important;
            font-family: {$more3_btn_font_stack} !important;
            border-radius: {$more3_btn_radius}px !important;
            flex: 0 0 auto !important;
            padding: 6px 14px !important;
        }
        {$sel_more3}:hover {
            background: {$more3_btn_bg} !important;
            color: {$more3_btn_color} !important;
        }
        /* Add Service for Person buttons */
        {$sel_for_person} {
            background: {$add_for_person_btn_bg} !important;
            color: {$add_for_person_btn_color} !important;
            font-size: {$add_for_person_btn_size}px !important;
            border-radius: {$add_for_person_btn_radius}px !important;
            flex: 0 0 auto !important;
            padding: 6px 12px !important;
        }
        {$sel_for_person}:hover {
            background: {$add_for_person_btn_bg} !important;
            color: {$add_for_person_btn_color} !important;
        }
        /* Add Guest button -- outline style, border follows the text colour */
        {$sel_add_guest} {
            background: {$add_guest_btn_bg} !important;
            color: {$add_guest_btn_color} !important;
            font-size: {$add_guest_btn_size}px !important;
            border: 1.5px solid {$add_guest_btn_color} !important;
            border-radius: {$add_guest_btn_radius}px !important;
            flex: 0 0 auto !important;
            padding: 6px 12px !important;
        }
        {$sel_add_guest}:hover {
            background: {$add_guest_btn_bg} !important;
            color: {$add_guest_btn_color} !important;
        }
        /* Confirm / Submit button */
        {$sel_submit} {
            background: {$conf_btn_bg} !important;
            border-color: {$conf_btn_bg} !important;
            {$conf_btn_shadow}
            color: {$conf_btn_color} !important;
            font-size: {$conf_btn_size}px !important;
            font-family: {$conf_btn_font_stack} !important;
            border-radius: {$conf_btn_radius}px !important;
        }
        {$sel_submit}:hover {
            background: {$conf_btn_bg} !important;
            border-color: {$conf_btn_bg} !important;
            color: {$conf_btn_color} !important;
        }
        {$sel_submit_one} {
            background: {$conf_single_btn_bg} !important;
            border-color: {$conf_single_btn_bg} !important;
            {$conf_single_btn_shadow}
            color: {$conf_single_btn_color} !important;
            font-size: {$conf_single_btn_size}px !important;
            border-radius: {$conf_single_btn_radius}px !important;
        }
        {$sel_submit_one}:hover {
            background: {$conf_single_btn_bg} !important;
            border-color: {$conf_single_btn_bg} !important;
            color: {$conf_single_btn_color} !important;
        }
        /* Darkening on hover is done with a filter so it works for any colour
           the merchant picks, including ones we cannot compute a shade for. */
        {$sel_back}:hover, {$sel_next}:hover, {$sel_more}:hover,
        {$sel_more2}:hover, {$sel_more3}:hover, {$sel_for_person}:hover,
        {$sel_add_guest}:hover, {$sel_submit}:hover, {$sel_add}:hover { filter: brightness(0.92); }
        ";

        wp_add_inline_style('sbs-frontend-css', $custom_css);
        wp_add_inline_style( $final_style_handle, $btn_overrides_css );

        // Button labels for JS rendering
        $btn_labels = array(
            'add'            => sanitize_text_field($add_btn_label)  ?: 'Add',
            'back'           => sanitize_text_field($back_btn_label) ?: 'Back',
            'cont'           => sanitize_text_field($cont_btn_label) ?: 'Continue',
            'more'           => sanitize_text_field($more_btn_label) ?: '+ Add More Services',
            'more2'          => sanitize_text_field($more2_btn_label) ?: '+ Add More Services',
            'more3'          => sanitize_text_field($more3_btn_label) ?: '+ Add More Services',
            'add_for_person' => sanitize_text_field($add_for_person_btn_label) ?: 'Add service for',
            'add_guest'      => sanitize_text_field($add_guest_btn_label) ?: 'Add guest together',
            'confirm'        => sanitize_text_field($conf_btn_label) ?: 'Confirm All Bookings',
            'confirm_single' => sanitize_text_field($conf_single_btn_label) ?: 'Confirm Booking',
        );

        // Get logged-in user's display name/initial for the guest review screen
        $current_user      = wp_get_current_user();
        $client_name       = $current_user && $current_user->ID ? $current_user->display_name : '';
        $client_initial    = $client_name ? strtoupper( substr( $client_name, 0, 1 ) ) : 'Y';

        $booking_timezone = yatoon_timezone();
        $deposit_active = yatoon_is_pro()
                          && get_option('yatoon_deposit_enabled', '0') === '1'
                          && get_option('yatoon_deposit_gateway', 'stripe') === 'stripe'
                          && !( class_exists( 'Yatoon_Square_Payments' ) && Yatoon_Square_Payments::is_enabled() )
                          && !empty(get_option('yatoon_deposit_stripe_key', ''))
                          && !empty(YATOON_Crypto::get_option('yatoon_deposit_stripe_secret', ''));

        // 4.6.2: Stripe's floor is 50 minor units, which is 0.50 in a decimal
        // currency but a whole 50 in a zero-decimal one (JPY, KRW, VND...).
        // Resolve it here, in major units, so the booking form can show the
        // real deposit without shipping its own currency table. Falls back to
        // the decimal case if the Pro Stripe class is not loaded.
        $yatoon_deposit_minimum = 0.5;
        if ( $deposit_active && class_exists( 'Yatoon_Stripe' ) ) {
            $yatoon_deposit_currency = Yatoon_Stripe::currency();
            $yatoon_deposit_minimum  = in_array( $yatoon_deposit_currency, Yatoon_Stripe::zero_decimal_currencies(), true )
                ? (float) Yatoon_Stripe::minimum_amount()
                : (float) Yatoon_Stripe::minimum_amount() / 100;
        }
        $paypal_active = yatoon_is_pro()
                         && class_exists('Yatoon_PayPal')
                         && !( class_exists('Yatoon_Square_Payments') && Yatoon_Square_Payments::is_enabled() )
                         && Yatoon_PayPal::is_enabled();
        $square_payments_active = yatoon_is_pro()
                                  && class_exists('Yatoon_Square_Payments')
                                  && Yatoon_Square_Payments::is_enabled();
        $paypal_sdk = $paypal_active ? add_query_arg(array(
            'client-id'  => get_option('yatoon_paypal_client_id', ''),
            'currency'   => strtoupper(sanitize_key(get_option('yatoon_currency', 'USD'))),
            'components' => 'buttons',
            'intent'     => 'capture',
        ), 'https://www.paypal.com/sdk/js') : '';
        $square_sdk = $square_payments_active
            ? ( get_option('yatoon_square_environment', 'production') === 'sandbox'
                ? 'https://sandbox.web.squarecdn.com/v1/square.js'
                : 'https://web.squarecdn.com/v1/square.js' )
            : '';
        wp_localize_script('sbs-frontend-js', 'yatoonData', array(
            'ajax_url'          => admin_url('admin-ajax.php'),
            'nonce'             => wp_create_nonce('yatoon_frontend_nonce'),
            'turnstileEnabled'  => get_option('yatoon_turnstile_enabled', '0') === '1' && !empty(get_option('yatoon_turnstile_site_key')),
            'turnstileSiteKey'  => get_option('yatoon_turnstile_site_key', ''),
            'siteTimezone'      => $booking_timezone->getName(),
            // 4.4.3: tax so the summary can show what the customer will
            // actually be charged. Without this the panel showed the net price
            // while the server stored the gross one -- on a tax-exclusive shop
            // the customer confirmed $25.00 and was billed $27.50, directly
            // under a "Review your total before confirming" promise.
            'tax'               => array(
                'enabled'   => class_exists( 'YATOON_Tax' ) && YATOON_Tax::is_enabled(),
                'rate'      => class_exists( 'YATOON_Tax' ) ? YATOON_Tax::default_rate() : 0,
                'inclusive' => class_exists( 'YATOON_Tax' ) && YATOON_Tax::prices_include_tax(),
                'label'     => class_exists( 'YATOON_Tax' ) ? YATOON_Tax::label() : __( 'Tax', 'yatoon-booking-system' ),
            ),
            // 4.4.3: minutes east of UTC for the SALON's timezone, right now.
            //
            // The date picker used to derive "today" from the visitor's own
            // clock, so a customer in a different timezone from the salon saw
            // the wrong day pre-selected -- and on a UTC+8 salon viewed from
            // the US, the landing state was an immediate "this date has
            // already passed". The calendar being sold belongs to the salon,
            // so the salon's clock decides what "today" is.
            //
            // Sent as an offset rather than relying on siteTimezone because a
            // site configured with a raw UTC offset has no Olson name for the
            // browser to resolve. Computed server-side so DST is already
            // applied for the current moment.
            'siteUtcOffsetMin'  => (int) round( $booking_timezone->getOffset( new DateTime( 'now', new DateTimeZone( 'UTC' ) ) ) / 60 ),
            'locationId'        => '',
            'clientName'        => sanitize_text_field( $client_name ),
            'clientInitial'     => sanitize_text_field( $client_initial ),
            'btnLabels'         => $btn_labels,
            'portalUrl'         => esc_url_raw(get_option('yatoon_portal_url', '')),
            'portalLabel'       => sanitize_text_field(get_option('yatoon_portal_label', 'My Appointments')),
            'bookNewUrl'        => esc_url_raw(get_option('yatoon_book_new_url', '')),
            'headerOffset'      => intval(get_option('yatoon_header_offset', 0)),
            'multiApptEnabled'  => get_option('yatoon_multi_appointment', '1') === '1',
            'maxGuestsTogether' => $max_guests_together,
            'immersiveMode'     => get_option('yatoon_booking_immersive_mode', '1') === '1',
            'serviceOptions'     => $this->get_preloaded_service_options(),
            'serviceStaff'       => $this->get_preloaded_service_staff(),
            'bookingLabels'       => array(
                'servicePopular'    => sanitize_text_field(get_option('yatoon_service_popular_label', 'Popular')),
                'customizeKicker'   => sanitize_text_field(get_option('yatoon_option_customize_kicker', 'Customize your service')),
                'relatedEyebrow'    => sanitize_text_field(get_option('yatoon_option_related_eyebrow', 'Related choices')),
                'relatedTitle'      => sanitize_text_field(get_option('yatoon_option_related_title', 'Choose what fits your visit')),
                'relatedHint'       => sanitize_text_field(get_option('yatoon_option_related_hint', 'Tap to select')),
                'mainGroupLabel'    => sanitize_text_field(get_option('yatoon_option_main_group_label', 'Choose one option')),
            'mainGroupHelp'     => sanitize_text_field(get_option('yatoon_option_main_group_help', __( 'Pick the main style or variation for this service.', 'yatoon-booking-system' ))),
                'addonGroupLabel'   => sanitize_text_field(get_option('yatoon_option_addon_group_label', 'Recommended add-ons')),
            'addonGroupHelp'    => sanitize_text_field(get_option('yatoon_option_addon_group_help', __( 'Optional extras you can add to this appointment.', 'yatoon-booking-system' ))),
                'recommendedBadge'  => sanitize_text_field(get_option('yatoon_option_recommended_badge_label', 'Recommended')),
                'popularBadge'      => sanitize_text_field(get_option('yatoon_option_popular_badge_label', 'Popular')),
                'addonBadge'        => sanitize_text_field(get_option('yatoon_option_addon_badge_label', 'Add-on')),
            ),
            'policyEnabled'     => get_option('yatoon_policy_enabled', '0') === '1',
            'policyRequireAck'  => get_option('yatoon_policy_require_ack', '1') === '1',
            'consentEnabled'    => get_option('yatoon_consent_enabled', '0') === '1',
            'couponsEnabled'    => yatoon_is_pro() && get_option('yatoon_enable_coupons', '0') === '1',
            'dynamicPricingEnabled' => yatoon_is_pro() && class_exists( 'YATOON_Dynamic_Pricing' ) && get_option( 'yatoon_dynamic_pricing_enabled', '0' ) === '1',
            'depositEnabled'    => $deposit_active,
            // 4.6.2: the service cards could only say "Secure deposit available"
            // -- the actual figure did not appear until checkout, and burying a
            // cost until the last step is a classic reason people abandon a
            // booking. These three values are everything the client needs to
            // show the same number the server will charge:
            //     deposit = min( fixed-or-percent of total, total )
            //     then floored at Stripe's per-currency minimum
            // which mirrors Yatoon_Stripe::calculate_deposit_cents(). The
            // minimum is handed over in MAJOR units so the client never has to
            // carry its own list of zero-decimal currencies (50 JPY, but 0.50 USD).
            'depositType'       => $deposit_active ? sanitize_key( (string) get_option( 'yatoon_deposit_type', 'fixed' ) ) : '',
            'depositAmount'     => $deposit_active ? (float) get_option( 'yatoon_deposit_amount', 20 ) : 0,
            'depositMinimum'    => $deposit_active ? $yatoon_deposit_minimum : 0,
            'stripePublishableKey' => sanitize_text_field(get_option('yatoon_deposit_stripe_key', '')),
            'stripeSdkUrl'      => $deposit_active ? 'https://js.stripe.com/v3/' : '',
            'paypalPaymentsEnabled' => $paypal_active,
            'paypalSdkUrl'      => esc_url_raw( $paypal_sdk ),
            'squarePaymentsEnabled' => $square_payments_active,
            'squareSdkUrl'      => esc_url_raw( $square_sdk ),
            'squarePaymentMode'  => class_exists('Yatoon_Square_Payments') ? Yatoon_Square_Payments::payment_mode() : 'deposit',
            'squarePaymentMethods' => class_exists('Yatoon_Square_Payments') ? Yatoon_Square_Payments::enabled_methods() : array('card'),
            'squareApplicationId'=> sanitize_text_field(get_option('yatoon_square_application_id', '')),
            'squareLocationId'   => sanitize_text_field( yatoon_external_location_id() ),
            'squareEnvironment'  => sanitize_key(get_option('yatoon_square_environment', 'production')),
            'squareCountryCode'  => strtoupper(sanitize_text_field(get_option('yatoon_square_country_code', 'US'))),
            'currency'           => strtoupper(sanitize_text_field(get_option('yatoon_currency', 'USD'))),
            'waitlistEnabled'   => get_option('yatoon_waitlist_enabled', '1') === '1',
            'ajaxUrl'           => admin_url('admin-ajax.php'),
            'verifyEmailOtp'    => get_option('yatoon_verify_email_otp', '0'),
            'verifySmsOtp'      => get_option('yatoon_verify_sms_otp', '0'),
            'phoneMode'         => get_option('yatoon_phone_mode', 'us_only'),
            'useIpGeo'          => get_option('yatoon_use_ip_geo_phone_detect', '0') === '1',
            'textUsEnabled'     => get_option('yatoon_text_us_enabled', '1') === '1',
            'whatsappEnabled'   => get_option('yatoon_whatsapp_enabled', '0') === '1',
            'whatsappNumber'    => preg_replace('/[^0-9]/', '', get_option('yatoon_whatsapp_number', get_option('yatoon_business_phone', ''))),
            'whatsappLabel'     => sanitize_text_field(get_option('yatoon_whatsapp_label', 'Message us on WhatsApp')),
            'successNote'       => sanitize_text_field(get_option('yatoon_success_action_note', __( 'Your booking is confirmed. You can add it to your calendar, manage changes, or book another visit.', 'yatoon-booking-system' ))),
            'isPro'             => yatoon_is_pro(),
            'rebookingNudgeText'=> sanitize_text_field(get_option('yatoon_rebooking_nudge_text', __( 'Most clients rebook every 2-3 weeks. Want us to remind you?', 'yatoon-booking-system' ))),
            'rebookingNudgeBtn' => sanitize_text_field(get_option('yatoon_rebooking_nudge_btn', 'Book next visit')),
            'availabilityHelp'  => sanitize_text_field(get_option('yatoon_availability_help_text', __( 'Try another date, choose another staff member, or text us and we can help.', 'yatoon-booking-system' ))),
            'ai'                => array(
                'serviceHelperEnabled' => get_option('yatoon_ai_service_helper_enabled', '1') === '1',
                'available'             => class_exists('YATOON_AI') && YATOON_AI::is_text_available(),
                'status'                => class_exists('YATOON_AI') ? YATOON_AI::status_message() : __('AI requires WordPress 7.0 or newer.', 'yatoon-booking-system'),
                'isPro'                 => yatoon_is_pro(),
            ),
            'strings'    => array(
                'select_service'   => __('Select a service', 'yatoon-booking-system'),
                'select_options'   => __('Select options', 'yatoon-booking-system'),
                'select_staff'     => __('Select staff member', 'yatoon-booking-system'),
                'select_date_time' => __('Select date & time', 'yatoon-booking-system'),
                'your_information' => __('Your information', 'yatoon-booking-system'),
                'confirm_booking'  => __('Confirm booking', 'yatoon-booking-system'),
                'booking_success'  => __('Booking confirmed!', 'yatoon-booking-system'),
                'booking_error'    => __('Booking failed. Please try again.', 'yatoon-booking-system'),
                'read_more'        => __('Read more', 'yatoon-booking-system'),
                'show_less'        => __('Show less', 'yatoon-booking-system'),
                'rating'           => __('Rating', 'yatoon-booking-system'),
                'checking_availability' => __('Checking availability...', 'yatoon-booking-system'),
            ),
        ));

        // Privacy-safe funnel telemetry. A random per-tab identifier measures
        // where the journey loses customers without storing names, email,
        // phone, IP-derived identifiers, or free-form field values.
        wp_add_inline_script( 'sbs-frontend-js', <<<'YATOON_FUNNEL_JS'
(function(){
    if(!window.yatoonData||!yatoonData.ajaxUrl||!yatoonData.nonce)return;
    var sessionKey='yatoon_funnel_session_v1',sentKey='yatoon_funnel_sent_v1';
    function randomHex(){var bytes=new Uint8Array(24);if(window.crypto&&crypto.getRandomValues){crypto.getRandomValues(bytes);return Array.from(bytes,function(v){return v.toString(16).padStart(2,'0');}).join('');}return(String(Date.now())+Math.random().toString(16).slice(2)).replace(/[^a-f0-9]/g,'').padEnd(32,'0');}
    var session=sessionStorage.getItem(sessionKey)||randomHex();sessionStorage.setItem(sessionKey,session);
    var sent={};try{sent=JSON.parse(sessionStorage.getItem(sentKey)||'{}');}catch(e){}
    function track(event,extra){var marker=event+(extra&&extra.error_code?':'+extra.error_code:'');if(sent[marker])return;sent[marker]=true;sessionStorage.setItem(sentKey,JSON.stringify(sent));var body=new URLSearchParams({action:'yatoon_track_funnel',nonce:yatoonData.nonce,event:event,session_hash:session,client_timezone:Intl.DateTimeFormat().resolvedOptions().timeZone||''});Object.keys(extra||{}).forEach(function(key){if(extra[key])body.set(key,String(extra[key]));});fetch(yatoonData.ajaxUrl,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded;charset=UTF-8'},body:body.toString(),keepalive:true}).catch(function(){});}
    function serviceId(target){var row=target&&target.closest?target.closest('[data-service-id]'):null;return row?row.getAttribute('data-service-id'):'';}
    document.addEventListener('click',function(e){if(e.target.closest('.sbs-service-item,.sbs-book-btn'))track('select_service',{service_id:serviceId(e.target)});if(e.target.closest('.sbs-time-slot'))track('select_time',{});if(e.target.closest('.sbs-btn-submit,#sbs-confirm-booking,#sbs-square-pay-button,[data-payment-provider]'))track('payment_started',{});},true);
    function visible(selector){var el=document.querySelector(selector);return !!(el&&getComputedStyle(el).display!=='none'&&el.getClientRects().length);}
    function scan(){if(visible('.sbs-step-5,.sbs-customer-form'))track('enter_details',{});if(visible('.sbs-step-success'))track('booking_success',{});var error=document.querySelector('.sbs-error:not([hidden]),.sbs-booking-error:not([hidden])');if(error&&visible('.sbs-error:not([hidden]),.sbs-booking-error:not([hidden])'))track('booking_error',{error_code:error.getAttribute('data-error-code')||'booking_error'});}
    document.addEventListener('DOMContentLoaded',function(){track('view_services',{});scan();var preset=document.querySelector('.sbs-booking-container[data-preselect-service]');var id=preset&&preset.getAttribute('data-preselect-service');if(id&&/^\d+$/.test(id)){var card=document.querySelector('.sbs-service-item[data-service-id="'+id+'"]');if(card)setTimeout(function(){card.click();},80);}new MutationObserver(scan).observe(document.body,{subtree:true,attributes:true,attributeFilter:['style','class']});});
})();
YATOON_FUNNEL_JS
        );

        // Payment SDKs are deliberately not enqueued on page load. The booking
        // client loads only the configured provider when checkout actually needs it.
    }

    /**
     * Booking shortcode
     */
    public function booking_shortcode( $atts ) {
        $this->mark_customer_page_dynamic();
        if ( class_exists( 'YATOON_Customer_Portal' ) && YATOON_Customer_Portal::get_instance() ) {
            YATOON_Customer_Portal::get_instance()->bootstrap_session();
        }
        yatoon_record_onboarding_milestone( 'booking_page_opened' );
        $atts = shortcode_atts(array(
            'location'    => '', // Documented public attribute; kept alongside legacy location_id.
            'location_id' => '',
            'service_id'  => '',
            'staff_id'    => '',
            'category'    => '',
            'mode'        => 'standard',
        ), $atts);
        // Service-menu, discovery, rebook, and waitlist links all converge on
        // this one safe public parameter. Shortcode attributes still win.
        if ( empty( $atts['service_id'] ) && isset( $_GET['yatoon_service'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only preselection.
            $atts['service_id'] = absint( wp_unslash( $_GET['yatoon_service'] ) );
        }
        if ( empty( $atts['location'] ) && empty( $atts['location_id'] ) && isset( $_GET['yatoon_location'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only preselection.
            $atts['location'] = sanitize_title( wp_unslash( $_GET['yatoon_location'] ) );
        }
        $atts['service_id'] = absint( $atts['service_id'] );
        $atts['staff_id']   = absint( $atts['staff_id'] );
        $atts['category']   = sanitize_text_field( $atts['category'] );
        $atts['mode']       = in_array( $atts['mode'], array( 'standard', 'landing' ), true ) ? $atts['mode'] : 'standard';

        $location_key = '' !== (string) $atts['location_id'] ? $atts['location_id'] : $atts['location'];
        $yatoon_location = null;
        if ( get_option( 'yatoon_multi_location_enabled', '0' ) === '1' && class_exists( 'YATOON_Locations' ) ) {
            $yatoon_location = YATOON_Locations::resolve( $location_key );
        }
        $this->render_frontend_assets();
        $this->localize_booking_context( $yatoon_location );

        ob_start();
        $this->render_booking_form($atts, $yatoon_location);
        $html = ob_get_clean();
        if ( ! empty( $_GET['yatoon_waitlist_offer'] ) && ! empty( $_GET['yatoon_offer_date'] ) && ! empty( $_GET['yatoon_offer_time'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only offer context; checkout revalidates availability.
            $date = sanitize_text_field( wp_unslash( $_GET['yatoon_offer_date'] ) );
            $time = sanitize_text_field( wp_unslash( $_GET['yatoon_offer_time'] ) );
            $offer = '<div class="sbs-waitlist-offer-context" role="status"><strong>' . esc_html__( 'Your waitlist opening is ready', 'yatoon-booking-system' ) . '</strong><span>' . esc_html( sprintf( __( '%1$s at %2$s — complete the steps below to confirm availability.', 'yatoon-booking-system' ), date_i18n( get_option( 'date_format' ), strtotime( $date ) ), $time ) ) . '</span></div>';
            $html = $offer . $html;
            wp_add_inline_style( 'sbs-frontend-css', '.sbs-waitlist-offer-context{display:grid;gap:3px;max-width:1180px;margin:0 auto 14px;padding:13px 16px;border:1px solid #86efac;border-radius:13px;background:#f0fdf4;color:#14532d}.sbs-waitlist-offer-context span{font-size:13px}' );
        }
        return $html;
    }

    public function catalog_shortcode( $atts ) {
        $atts = shortcode_atts(array(
            'category' => '',
            'limit'    => 50,
            'cols'     => get_option('yatoon_catalog_cols', 3),
        ), $atts);
        ob_start();
        include SBS_PLUGIN_DIR . 'public/views/catalog.php';
        return ob_get_clean();
    }

    public function gallery_shortcode( $atts ) {
        $atts = shortcode_atts(array(
            'service_id' => 0,
            'category'   => '',
            'limit'      => 50,
            'mode'       => get_option('yatoon_gallery_mode', 'grid'),
            'cols'       => get_option('yatoon_gallery_cols', 5),
            'gap'        => get_option('yatoon_gallery_gap', 6),
            'ratio'      => get_option('yatoon_gallery_ratio', '1/1'),
            'lightbox'   => get_option('yatoon_gallery_lightbox', '1'),
            'autoplay'   => get_option('yatoon_gallery_autoplay', '1'),
            'interval'   => get_option('yatoon_gallery_interval', 3500),
            'captions'   => get_option('yatoon_gallery_captions', '0'),
            'dots'       => '1',
            'arrows'     => '1',
            'height'     => get_option('yatoon_gallery_height', '460px'),
            'mobile_height' => '',
            'book_label' => get_option('yatoon_gallery_book_label', get_option('yatoon_catalog_book_label', 'Book Now')),
        ), $atts);
        ob_start();
        include SBS_PLUGIN_DIR . 'public/views/gallery.php';
        return ob_get_clean();
    }

    /**
     * Render booking form
     */
    private function localize_booking_context( $location ) {
        $location_id = $location ? (string) $location->id : '';
        $timezone    = yatoon_timezone( $location_id ?: null );
        $payload     = array(
            'locationId'       => $location_id,
            'siteTimezone'     => $timezone->getName(),
            'siteUtcOffsetMin' => (int) round( $timezone->getOffset( new DateTime( 'now', new DateTimeZone( 'UTC' ) ) ) / 60 ),
            'squareLocationId' => sanitize_text_field( yatoon_external_location_id( $location_id ?: null ) ),
        );
        wp_add_inline_script(
            'sbs-frontend-js',
            'window.yatoonData=Object.assign(window.yatoonData||{},' . wp_json_encode( $payload ) . ');',
            'before'
        );
    }

    private function render_booking_form( $atts, $resolved_location = null ) {
        global $wpdb;

        // -- Resolve location --------------------------------------------------
        $yatoon_location = $resolved_location;

        // Get service list
        $table_services = $wpdb->prefix . 'yatoon_services';
        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $services = $wpdb->get_results("
            SELECT * FROM $table_services
            WHERE is_active = 1
            ORDER BY sort_order ASC, name ASC
        ");

        // Use Square-style form
        include SBS_PLUGIN_DIR . 'public/views/booking-form-square.php';
    }
}


// 4.6.2: Specificity escalation for the "Button Labels, Fonts & Colours"
// settings. The skin/template stylesheets style the same buttons with
// !important at (0,2,0) and up; a bare class override at (0,1,0) loses every
// time no matter which stylesheet is enqueued last. Repeating the class raises
// the merchant's explicit choice above them without depending on the page
// markup. The "html body" prefix adds two element steps so we also outrank
// same-specificity rules that a theme prints later in the document.
if ( ! function_exists( 'yatoon_btn_sel' ) ) {
    function yatoon_btn_sel( $target, $ancestor = '' ) {
        $target   = (string) $target;
        $ancestor = (string) $ancestor;
        // Five repeats clears the worst competitor in the bundle today
        // (.sbs-btn-submit:not(:disabled):not(.is-disabled):hover at (0,4,0))
        // with room to spare for future skins.
        $repeated = str_repeat( $target, 5 );
        $prefix   = 'html body';
        if ( '' !== $ancestor ) {
            $prefix .= ' ' . $ancestor;
        }
        return $prefix . ' ' . $repeated;
    }
}

// 4.6.2: Template skins ship a coloured glow (box-shadow) alongside their
// button background. Once a merchant picks their own colour that glow is left
// behind and reads as a mismatched halo, so recolour it from their choice.
// Returns a ready-to-inline declaration, or an empty string when the value is
// not a plain hex colour we can convert - in that case the existing shadow is
// left untouched rather than guessed at.
if ( ! function_exists( 'yatoon_btn_glow' ) ) {
    function yatoon_btn_glow( $hex ) {
        $hex = trim( (string) $hex );
        if ( ! preg_match( '/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $hex ) ) {
            return '';
        }
        $hex = ltrim( $hex, '#' );
        if ( 3 === strlen( $hex ) ) {
            $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
        }
        $r = hexdec( substr( $hex, 0, 2 ) );
        $g = hexdec( substr( $hex, 2, 2 ) );
        $b = hexdec( substr( $hex, 4, 2 ) );
        return "box-shadow: 0 4px 14px rgba({$r},{$g},{$b},.25) !important;";
    }
}
