HEX
Server: Apache/2.4.68 (Debian)
System: Linux as-cs-widget-demo-us-central1 6.1.0-44-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.164-1 (2026-03-09) x86_64
User: root (0)
PHP: 8.2.32
Disabled: NONE
Upload Files
File: /var/www/kevin-demo/wp-content/plugins/allspice/allspice.php
<?php
/**
 * Plugin Name: Allspice
 * Description: Loads the Allspice widget and analytics packages.
 * Version: 1.4.37
 * Author: Allspice Labs, Inc.
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: allspice
 */

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

if (defined('ALLSPICE_WIDGET_LOADER_BOOTED')) return;
define('ALLSPICE_WIDGET_LOADER_BOOTED', true);

define('ALLSPICE_PLUGIN_FILE', __FILE__);
if (!defined('ALLSPICE_PLUGIN_VERSION')) {
    define('ALLSPICE_PLUGIN_VERSION', allspice_get_plugin_version());
}
define('ALLSPICE_WIDGET_LOADER_URL', plugin_dir_url(__FILE__));
define('ALLSPICE_WIDGET_LOADER_PATH', plugin_dir_path(__FILE__));
define('ALLSPICE_DEBUG_PARAM', 'allspice_debug');
define('ALLSPICE_DEBUG_NONCE_ACTION', 'allspice_debug');
define('ALLSPICE_DEBUG_NONCE_PARAM', 'allspice_dbg_nonce');

require_once plugin_dir_path(__FILE__) . 'includes/env.php';
require_once plugin_dir_path(__FILE__) . 'includes/api.php';
require_once plugin_dir_path(__FILE__) . 'includes/settings.php';
require_once plugin_dir_path(__FILE__) . 'includes/cron.php';
require_once plugin_dir_path(__FILE__) . 'includes/webhook.php';
require_once plugin_dir_path(__FILE__) . 'includes/url_key.php';
require_once plugin_dir_path(__FILE__) . 'includes/db_recipes.php';
require_once plugin_dir_path(__FILE__) . 'includes/recipes_sync.php';
require_once plugin_dir_path(__FILE__) . 'includes/seo.php';
require_once ALLSPICE_WIDGET_LOADER_PATH . 'includes/blocks.php';

register_activation_hook(__FILE__, function () {
    allspice_create_recipes_table();
    allspice_activate_plugin();
});

register_deactivation_hook(__FILE__, function () {
    allspice_deactivate_plugin();
});

add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) {
    $url = admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE);
    array_unshift($links, '<a href="' . esc_url($url) . '">Settings</a>');
    return $links;
});

function allspice_get_plugin_version(): string {
    static $ver = null;
    if ($ver !== null) return $ver;
    if (!function_exists('get_file_data')) require_once ABSPATH . 'wp-includes/functions.php';
    $data = get_file_data(ALLSPICE_PLUGIN_FILE, array('Version' => 'Version'), 'plugin');
    $ver = (!empty($data['Version']) ? $data['Version'] : '0.0.0');
    return $ver;
}

function allspice_get_widget_build(): string {
    $files = [
        ALLSPICE_WIDGET_LOADER_PATH . 'dist/allspicewidget.css',
        ALLSPICE_WIDGET_LOADER_PATH . 'dist/allspicewidget.umd.cjs',
        ALLSPICE_WIDGET_LOADER_PATH . 'dist/page.js',
    ];
    $max = 0;
    foreach ($files as $f) {
        if (file_exists($f)) {
            $max = max($max, filemtime($f));
        }
    }
    return $max ? (string)$max : (string)time();
}

function allspice_assets_version(): string {
    return allspice_get_plugin_version() . '.' . allspice_get_widget_build();
}
    
function allspice_is_debug(): bool {
    if (!is_user_logged_in() || !current_user_can('manage_options')) return false;
    $debug = isset($_GET[ALLSPICE_DEBUG_PARAM])
        ? sanitize_text_field(wp_unslash($_GET[ALLSPICE_DEBUG_PARAM]))
        : '';
    if ($debug !== '1') return false;
    $nonce = isset($_GET[ALLSPICE_DEBUG_NONCE_PARAM])
        ? sanitize_text_field(wp_unslash($_GET[ALLSPICE_DEBUG_NONCE_PARAM]))
        : '';
    return (bool) wp_verify_nonce($nonce, ALLSPICE_DEBUG_NONCE_ACTION);
}

function allspice_console_log(string $label, $data = null): void {
    if (!allspice_is_debug()) return;
    if (!isset($GLOBALS['allspice_debug_logs']) || !is_array($GLOBALS['allspice_debug_logs'])) {
        $GLOBALS['allspice_debug_logs'] = array();
    }
    $GLOBALS['allspice_debug_logs'][] = array('label' => $label, 'data' => $data);
}
    
add_filter('script_loader_tag', function ($tag, $handle, $src) {
    $is_allspice =
        ($handle === 'allspice-widget-page') ||
        ($handle === 'allspice-widget-umd') ||
        (is_string($src) && (
            strpos($src, 'widget-dev.allspicelabs.com') !== false ||
            strpos($src, 'widget.allspicelabs.com') !== false ||
            strpos($src, 'allspicewidget.umd.cjs') !== false ||
            strpos($src, '/prod/page/') !== false ||
            strpos($src, '/dev/page/') !== false ||
            strpos($src, '/main.js') !== false
        )) ||
        (strpos($tag, 'id="allspice-widget-page-js"') !== false) ||
        (strpos($tag, 'id="allspice-widget-umd-js"') !== false);
    if ($is_allspice && strpos($tag, ' nowprocket') === false) {
        $tag = str_replace('<script ', '<script nowprocket ', $tag);
    }
    return $tag;
}, 10, 3);

add_filter('style_loader_tag', function ($html, $handle, $href, $media) {
    $is_allspice =
        ($handle === 'allspice-widget-style') ||
        (is_string($href) && (
            strpos($href, 'widget-dev.allspicelabs.com') !== false ||
            strpos($href, 'widget.allspicelabs.com') !== false ||
            strpos($href, 'allspicewidget.css') !== false
        ));
    if ($is_allspice && strpos($html, ' nowprocket') === false) {
        $html = str_replace("<link ", "<link nowprocket ", $html);
    }
    return $html;
}, 10, 4);

add_action('wp_footer', function () {
    if (!allspice_is_debug()) return;
    $logs = $GLOBALS['allspice_debug_logs'] ?? array();
    if (!$logs) return;
    $json = wp_json_encode($logs, JSON_UNESCAPED_SLASHES);
    $js = "try{(" . $json . ").forEach(function(x){console.log('[Allspice][PHP]', x);});}catch(e){}";
    wp_print_inline_script_tag($js);
}, 999);

function allspice_mark_needed(): void {
    $GLOBALS['allspice_widget_needed'] = true;
}

function allspice_shortcode_atts_to_string(array $atts): string {
    $parts = array();
    foreach ($atts as $k => $v) {
        $parts[] = sanitize_key((string)$k) . '="' . esc_attr((string)$v) . '"';
    }
    return implode(' ', $parts);
}

function allspice_is_legacy_floating_surface_button(string $id): bool {
    $id = trim($id);
    if ($id === '') {
        return false;
    }
    if (function_exists('allspice_is_rail_button_id')) {
        if (allspice_is_rail_button_id($id)) {
            return true;
        }
    } elseif ($id === 'WIDCON-d29x') {
        return true;
    }
    foreach (allspice_buttons_cache_get() as $button) {
        if (trim((string)($button['id'] ?? '')) !== $id) {
            continue;
        }
        $style = strtolower(
            trim((string)($button['style'] ?? ''))
        );
        return $style === 'floating';
    }
    return $id === 'WIDCON-ea5d';
}

function allspice_register_shortcodes(): void {
    add_shortcode('allspice_button', function ($atts) {
        allspice_mark_needed();
        $atts = shortcode_atts(array(
            'id' => '',
            'reserve_w' => '',
            'reserve_h' => '',
            'class' => '',
            'flow' => '0',
        ), (array)$atts, 'allspice_button');
        $flow = ((string)$atts['flow'] === '1');
        $id = trim((string)$atts['id']);
        if ($id === '') return '';
        if (
            allspice_is_float_variant() &&
            allspice_is_legacy_floating_surface_button($id)
        ) {
            return '';
        }
        $class = (string)$atts['class'];
        $w = preg_replace('/[^0-9]/', '', trim((string)$atts['reserve_w']));
        $h = preg_replace('/[^0-9]/', '', trim((string)$atts['reserve_h']));
        if ($w === '' || $h === '') {
            $d = allspice_premade_defaults($id);
            if ($d && ($w === '' || $h === '')) {
                if ($w === '') $w = (string)$d['reserve_w'];
                if ($h === '') $h = (string)$d['reserve_h'];
                if (!isset($atts['flow']) || (string)$atts['flow'] === '0') {
                  $flow = (bool)$d['flow'];
                }
            }
        }
        $w_px = ($w !== '' && ctype_digit($w)) ? $w . 'px' : '';
        $h_px = ($h !== '' && ctype_digit($h)) ? $h . 'px' : '';
        $style = '';
        if ($flow) {
            $style .= 'display:block;';
            $style .= 'width:100%;';
            if ($w_px) $style .= 'max-width:' . $w_px . ';';
            $style .= 'margin-left:auto;margin-right:auto;';
            if ($h_px) $style .= 'min-height:' . $h_px . ';';
        } else {
            $style .= 'display:inline-block;';
            if ($w_px) $style .= 'width:' . $w_px . ';';
            if ($h_px) $style .= 'height:' . $h_px . ';';
        }
        return '<span'
            . ' class="allspice-slot ' . esc_attr($class) . '"'
            . ' data-allspice-mode="premade"'
            . ' data-allspice-button="' . esc_attr($id) . '"'
            . ' data-allspice-layout="' . ($flow ? 'flow' : 'fixed') . '"'
            . ' style="' . esc_attr($style) . '"'
            . ' aria-busy="true"'
            . '></span>';
    });
    add_shortcode('Allspice_Button', function ($atts) {
        $atts = is_array($atts) ? $atts : array();
        return do_shortcode('[allspice_button ' . allspice_shortcode_atts_to_string($atts) . ']');
    });
}
add_action('init', 'allspice_register_shortcodes');

function allspice_mark_needed_on_shortcode_exec($output, $tag, $attr) {
    if ($tag === 'allspice_button' || $tag === 'Allspice_Button') allspice_mark_needed();
    return $output;
}
add_filter('do_shortcode_tag', 'allspice_mark_needed_on_shortcode_exec', 10, 3);

function allspice_detect_shortcode_in_content(): void {
    if (is_admin() || !is_singular()) return;
    global $post;
    if (!$post || empty($post->post_content)) return;
    if (has_shortcode($post->post_content, 'allspice_button') || has_shortcode($post->post_content, 'Allspice_Button')) allspice_mark_needed();
    if (
        strpos($post->post_content, 'data-allspice-id=') !== false
    ) {
        allspice_mark_needed();
    }
    allspice_console_log('[Allspice] detect_shortcode_in_content fired; found=' . (
      (has_shortcode($post->post_content, 'allspice_button') || has_shortcode($post->post_content, 'Allspice_Button') || strpos($post->post_content, 'data-allspice-id=') !== false)
      ? '1' : '0'
    ));
}
add_action('wp', 'allspice_detect_shortcode_in_content');

function allspice_allowed_html(): array {
    return array(
        'div' => array(
            'id' => true,
            'class' => true,
            'data-asx-hide' => true,
            'data-asx-side' => true,
            'data-asx-initial-dock' => true,
            'data-asx-page-kind' => true,
            'data-asx-surface' => true,
            'style' => true,
        ),
        'span' => array(
            'class' => true,
            'data-allspice-mode' => true,
            'data-allspice-button' => true,
            'data-allspice-layout' => true,
            'style' => true,
            'aria-busy' => true,
        ),
    );
}

function allspice_get_request_uri(): string {
    // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below.
    $v = isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '';
    $v = is_string($v) ? $v : '';
    $v = preg_replace('/[^\x20-\x7E]/', '', $v);
    $v = sanitize_text_field($v);
    return $v !== '' ? $v : '/';
}
    
function allspice_current_url_key(): string {
    $req_uri = allspice_get_request_uri();
    if ($req_uri === '') $req_uri = '/';
    $url = home_url($req_uri);
    return allspice_url_key($url);
}

function allspice_is_recipe_page($postObj = null): bool {
    if (is_admin()) return false;
    if (!is_singular()) return false;
    $k = allspice_current_url_key();
    if ($k === '') return false;
    if (function_exists('allspice_recipes_has_url_key')) {
        $is_recipe = allspice_recipes_has_url_key($k);
        if ($is_recipe) return true;
        return false;
    }
    return false;
}
    
function allspice_widget_needed_now(): bool {
//    if (allspice_is_debug()) return true;
    if (is_admin()) return false;
    return true;
//    if (!is_singular()) return false;
//    global $post;
//    if (!$post) return false;
//    if (allspice_is_recipe_page($post)) return true;
//    if (!empty($GLOBALS['allspice_widget_needed'])) return true;
//    if (allspice_has_manual_placements_in_post($post)) return true;
//    return false;
}

function allspice_has_manual_placements_in_post($postObj = null): bool {
    if (!$postObj) {
        global $post;
        $postObj = $post;
    }
    if (!$postObj || empty($postObj->post_content)) return false;
    $c = (string)$postObj->post_content;
    if (
        has_shortcode($c, 'allspice_button') ||
        has_shortcode($c, 'Allspice_Button') ||
        strpos($c, 'data-allspice-id=') !== false
        ) return true;
    if (function_exists('has_block')) {
        if (
            has_block('allspice/floating-button', $postObj) ||
            has_block('allspice/rail', $postObj) ||
            has_block('allspice/inline-chat', $postObj) ||
            has_block('allspice/inline-search', $postObj) ||
            has_block('allspice/inline-overview-style-1', $postObj) ||
            has_block('allspice/inline-overview-style-2', $postObj) ||
            has_block('allspice/inline-cooking-cta', $postObj)
            ) return true;
    }
    return false;
}

function allspice_cache_has_autoplaced_buttons(): bool {
    $buttons = allspice_buttons_cache_get();
    if (!$buttons) return false;
    $dbg_buttons = array();
    foreach (($buttons ?: array()) as $b) {
        $dbg_buttons[] = array(
            'id' => (string)($b['id'] ?? ''),
            'active' => !empty($b['active']) ? 1 : 0,
            'style' => (string)($b['style'] ?? ''),
            'placement' => (string)($b['placement'] ?? ''),
        );
    }
    allspice_console_log('[Allspice][PHP][floating] buttons_dump', $dbg_buttons);
    foreach ($buttons as $b) {
        if (empty($b['active'])) continue;
        $style = (string)($b['style'] ?? '');
        $placement = allspice_normalize_placement($style, (string)($b['placement'] ?? 'manual_only'));
        if ($style === 'inline') {
            if ($placement !== '' && $placement !== 'manual_only') return true;
        } else if ($style === 'floating') {
            if ($placement === 'automatic') return true;
        }
    }
    return false;
}

add_filter('the_content', 'allspice_autoplace_inline_into_content', 20);

function allspice_autoplace_inline_into_content(string $content): string {
    if (is_admin() || !is_singular()) return $content;
    //  if (!is_main_query() || !in_the_loop()) return $content; # genesis [KEEP]
    global $post;
    if (!$post) return $content;
    if (!allspice_is_recipe_page($post)) return $content;
    if (allspice_has_manual_inline_placements_in_post($post)) return $content;
    $buttons = allspice_buttons_cache_get();
    if (!$buttons) return $content;
    $groups = array();
    foreach ($buttons as $b) {
        if (empty($b['active'])) continue;
        if (($b['style'] ?? '') !== 'inline') continue;
        $style = (string)($b['style'] ?? '');
        $placement_raw = (string)($b['placement'] ?? 'manual_only');
        $placement = allspice_normalize_placement($style, $placement_raw);
        if ($placement === '' || $placement === 'manual_only') continue;
        $id = trim((string)($b['id'] ?? ''));
        if ($id === '') continue;
        if (!isset($groups[$placement])) $groups[$placement] = array();
        $groups[$placement][] = array(
            'id' => $id,
            'pad_top' => $b['pad_top'] ?? null,
            'pad_bottom' => $b['pad_bottom'] ?? null,
            'theme' => (isset($b['theme']) && is_array($b['theme'])) ? $b['theme'] : null,
        );
    }
    if (!$groups) return $content;
    foreach ($groups as $placement => $items) {
        $indexed = array();
        foreach ($items as $idx => $item) {
            $indexed[] = array(
                '__index' => $idx,
                '__item' => $item,
            );
        }
        usort($indexed, function ($a, $b) {
            $aItem = $a['__item'];
            $bItem = $b['__item'];
            $aId = is_array($aItem) ? (string)($aItem['id'] ?? '') : (string)$aItem;
            $bId = is_array($bItem) ? (string)($bItem['id'] ?? '') : (string)$bItem;
            $aInlineCta = ($aId === 'WIDCON-f92u' || $aId === 'WIDCON-a75g' || $aId === 'WIDCON-b64k') ? 0 : 1;
            $bInlineCta = ($bId === 'WIDCON-f92u' || $bId === 'WIDCON-a75g' || $bId === 'WIDCON-b64k') ? 0 : 1;
            if ($aInlineCta !== $bInlineCta) {
                return $aInlineCta <=> $bInlineCta;
            }
            return ((int)$a['__index']) <=> ((int)$b['__index']);
        });
        $sortedItems = array();
        foreach ($indexed as $row) {
            $sortedItems[] = $row['__item'];
        }
        $content = allspice_inject_inline_slots($content, $placement, $sortedItems);
    }
    return $content;
}

function allspice_render_slot_html(string $id, bool $flow = true, int $reserve_w = 720, int $reserve_h = 280): string {
    $atts = array(
        'id' => $id,
        'reserve_w' => (string)$reserve_w,
        'reserve_h' => (string)$reserve_h,
        'flow' => $flow ? '1' : '0',
    );
    return do_shortcode('[allspice_button ' . allspice_shortcode_atts_to_string($atts) . ']');
}
    
function allspice_autoplace_inline_spacing(string $placement, array $buttonRow): array {
    $top = 16;
    $bottom = 16;
    if (isset($buttonRow['theme']) && is_array($buttonRow['theme'])) {
        $t = $buttonRow['theme'];
        if (array_key_exists('padding_top', $t)) {
            $top = allspice_autoplace_clamp_int($t['padding_top'], 0, 200, $top);
        }
        if (array_key_exists('padding_bottom', $t)) {
            $bottom = allspice_autoplace_clamp_int($t['padding_bottom'], 0, 200, $bottom);
        }
    }
    return array('top' => $top, 'bottom' => $bottom);
}

function allspice_autoplace_clamp_int($v, int $min, int $max, int $fallback): int {
    if (function_exists('allspice_clamp_int')) {
        return allspice_clamp_int($v, $min, $max, $fallback);
    }
    if ($v === null) return $fallback;
    if (is_string($v)) $v = trim($v);
    if ($v === '' || !is_numeric($v)) return $fallback;
    $n = (int)$v;
    if ($n < $min) return $min;
    if ($n > $max) return $max;
    return $n;
}

function allspice_autoplace_button_spacing(array $buttonRow, string $placement): array {
    $sp  = allspice_autoplace_inline_spacing($placement, $buttonRow);
    $top = (int)($sp['top'] ?? 16);
    $bot = (int)($sp['bottom'] ?? 16);
    if (array_key_exists('pad_top', $buttonRow)) {
        $top = allspice_autoplace_clamp_int($buttonRow['pad_top'], 0, 200, $top);
    }
    if (array_key_exists('pad_bottom', $buttonRow)) {
        $bot = allspice_autoplace_clamp_int($buttonRow['pad_bottom'], 0, 200, $bot);
    }
    return array('top' => $top, 'bottom' => $bot);
}

function allspice_wrap_autoplaced_group_html(string $inner, string $placement): string {
    $style = 'display:flow-root;';
    return '<div class="asx-autoplace asx-autoplace--' . esc_attr($placement) . '" data-asx-autoplaced="' . esc_attr($placement) . '" style="' . esc_attr($style) . '">' . $inner . '</div>';
}

function allspice_wrap_autoplaced_slot_html(string $slotHtml, int $top, int $bottom): string {
    $top = max(0, min(200, $top));
    $bottom = max(0, min(200, $bottom));
    $style = 'display:flow-root;padding-top:' . $top . 'px;padding-bottom:' . $bottom . 'px;';
    return '<div class="asx-autoplace-slot" style="' . esc_attr($style) . '">' . $slotHtml . '</div>';
}
    
function allspice_inject_inline_slots(string $content, string $placement, array $items): string {
    $existingMarker = 'data-asx-autoplaced="' . $placement . '"';
    if (strpos($content, $existingMarker) !== false) {
        return $content;
    }
    $inner = "\n";
    $seenIds = array();
    foreach ($items as $it) {
        $id = is_array($it) ? (string)($it['id'] ?? '') : (string)$it;
        if ($id === '') continue;
        if (isset($seenIds[$id])) continue;
        $seenIds[$id] = true;
        $reserve_w = 920;
        $reserve_h = ($id === 'WIDCON-f92u') ? 180 : (($id === 'WIDCON-a75g' || $id === 'WIDCON-b64k') ? 220 : 320);
        $slot = allspice_render_slot_html($id, true, $reserve_w, $reserve_h);
        $buttonRow = is_array($it) ? $it : array('id' => $id);
        $sp = allspice_autoplace_button_spacing($buttonRow, $placement);
        $inner .= allspice_wrap_autoplaced_slot_html($slot, (int)$sp['top'], (int)$sp['bottom']) . "\n";
    }
    $inner .= "\n";
    $html = allspice_wrap_autoplaced_group_html($inner, $placement);
    switch ($placement) {
        case 'end_of_post':
            return $content . $html;
        case 'after_introduction':
            return allspice_insert_after_nth_paragraph($content, $html, 2);
        case 'after_first_section':
            return allspice_insert_after_first_heading($content, $html);
        case 'top_of_recipe_card':
            return allspice_insert_top_of_recipe_card($content, $html);
        case 'above_recipe_card':
            return allspice_insert_above_recipe_card($content, $html);
        case 'under_recipe_card':
            return allspice_insert_under_recipe_card($content, $html);
        case 'top_of_post':
            return allspice_insert_at_top_of_post($content, $html);
        default:
            return $content . $html;
    }
}
    
function allspice_insert_top_of_recipe_card(string $content, string $insertHtml): string {
    $bounds = allspice_find_wprm_recipe_bounds($content);
    if (!$bounds) {
        return $content;
    }
    $start = (int)$bounds['start'];
    return substr($content, 0, $start)
        . "\n" . $insertHtml . "\n"
        . substr($content, $start);
}

function allspice_insert_above_recipe_card(string $content, string $insertHtml): string {
    $container = allspice_find_first_wprm_recipe_container_open_tag($content);
    if (!$container) {
        return allspice_insert_top_of_recipe_card($content, $insertHtml);
    }
    $start = (int)$container['start'];
    $contentBeforeContainer = substr($content, 0, $start);
    if (preg_match(
        '/<(?P<tag>div|span|a)\b[^>]*\bid=(["\'])([^"\']+)\2[^>]*>\s*<\/(?P=tag)>\s*$/is',
        $contentBeforeContainer,
        $anchorMatch,
        PREG_OFFSET_CAPTURE
    )) {
        $start = (int)$anchorMatch[0][1];
    }
    return substr($content, 0, $start)
        . "\n" . $insertHtml . "\n"
        . substr($content, $start);
}

function allspice_insert_under_recipe_card(string $content, string $insertHtml): string {
    $bounds = allspice_find_wprm_recipe_bounds($content);
    if (!$bounds) {
        return $content;
    }
    $end = (int)$bounds['end'];
    return substr($content, 0, $end)
        . "\n" . $insertHtml . "\n"
        . substr($content, $end);
}
    
function allspice_insert_at_top_of_post(string $content, string $insertHtml): string {
    $hPos = null;
    if (preg_match('/<h[1-6]\b[^>]*>/i', $content, $m, PREG_OFFSET_CAPTURE)) {
        $hPos = $m[0][1];
    }
    $pPos = null;
    if (preg_match_all('/<p\b[^>]*>.*?<\/p>/is', $content, $pm, PREG_OFFSET_CAPTURE)) {
        foreach ($pm[0] as $match) {
            $pHtml = $match[0];
            $pos = $match[1];
            $text = html_entity_decode(wp_strip_all_tags($pHtml), ENT_QUOTES | ENT_HTML5, 'UTF-8');
            $text = preg_replace('/\x{00A0}/u', ' ', $text); // nbsp
            $text = trim(preg_replace('/\s+/u', ' ', $text));
            $textNoPunct = trim(preg_replace('/[[:punct:]\s]+/u', '', $text));
            if (mb_strlen($textNoPunct, 'UTF-8') >= 20) {
                $pPos = $pos;
                break;
            }
        }
    }
    $pos = null;
    if ($hPos !== null && $pPos !== null) {
        $pos = min($hPos, $pPos);
    } elseif ($hPos !== null) {
        $pos = $hPos;
    } elseif ($pPos !== null) {
        $pos = $pPos;
    }
    if ($pos !== null) {
        return substr($content, 0, $pos)
            . "\n" . $insertHtml . "\n"
            . substr($content, $pos);
    }
    return $insertHtml . "\n" . $content;
}

function allspice_insert_after_nth_paragraph(string $content, string $insertHtml, int $n): string {
    if ($n <= 0) return $insertHtml . $content;
    $parts = preg_split('/(<\/p>)/i', $content, -1, PREG_SPLIT_DELIM_CAPTURE);
    if (!$parts || count($parts) < 2) return $content . $insertHtml;
    $out = '';
    $pCount = 0;
    for ($i = 0; $i < count($parts); $i++) {
        $out .= $parts[$i];
        if (preg_match('/<\/p>/i', $parts[$i])) {
            $pCount++;
            if ($pCount === $n) $out .= "\n" . $insertHtml . "\n";
        }
    }
    if ($pCount < $n) $out .= "\n" . $insertHtml . "\n";
    return $out;
}

function allspice_insert_after_first_heading(string $content, string $insertHtml): string {
    foreach (array('/<\/h2>/i', '/<\/h3>/i') as $re) {
        if (preg_match($re, $content, $m, PREG_OFFSET_CAPTURE)) {
            $pos = $m[0][1] + strlen($m[0][0]);
            return substr($content, 0, $pos) . "\n" . $insertHtml . "\n" . substr($content, $pos);
        }
    }
    return allspice_insert_after_nth_paragraph($content, $insertHtml, 2);
}

if (!has_action('wp_footer', 'allspice_autoplace_floating')) {
    add_action('wp_footer', 'allspice_autoplace_floating', 5);
}

function allspice_is_rail_button_id(string $id): bool {
    return trim($id) === 'WIDCON-d29x';
}

function allspice_is_standard_floating_button(array $button): bool {
    $id = trim((string)($button['id'] ?? ''));
    $style = strtolower((string)($button['style'] ?? ''));
    return $style === 'floating' && $id !== '' && !allspice_is_rail_button_id($id);
}

function allspice_floating_candidate_allowed_on_current_page(array $button, bool $is_recipe): bool {
    if ($is_recipe) {
        return true;
    }
    $opts = (!empty($button['options']) && is_array($button['options']))
        ? $button['options']
        : array();
    return (($opts['show_on_all_pages'] ?? false) === true);
}

function allspice_autoplace_floating(): void {
    allspice_console_log('context in autoplace floating', array(
        'is_admin' => is_admin() ? 1 : 0,
        'is_singular' => is_singular() ? 1 : 0,
        'post_id' => isset($GLOBALS['post']) && $GLOBALS['post'] ? (string)$GLOBALS['post']->ID : null,
        'uri' => allspice_get_request_uri() ?: null,
    ));
    allspice_console_log('[Allspice][PHP][floating] ENTER uri=' . allspice_get_request_uri());
    if (is_admin()) {
        return;
    }
    if (allspice_is_float_variant()) {
        allspice_console_log(
            '[Allspice][PHP][floating] skipped legacy floating/rail surfaces for float variant'
        );
        return;
    }
    global $post;
    $is_recipe = ($post ? allspice_is_recipe_page($post) : false);
    $page_kind = $is_recipe ? 'recipe' : 'non_recipe';
    allspice_console_log('[Allspice][PHP][floating] page_kind=' . $page_kind . ' post_id=' . (string)($post->ID ?? ''));
    $has_manual_rail = ($post ? allspice_has_manual_rail_placements_in_post($post) : false);
    $has_manual_standard_float = ($post ? allspice_has_manual_standard_floating_placements_in_post($post) : false);
    allspice_console_log('[Allspice][PHP][floating] has_manual_rail=' . ($has_manual_rail ? '1' : '0'));
    allspice_console_log('[Allspice][PHP][floating] has_manual_standard_float=' . ($has_manual_standard_float ? '1' : '0'));
    $buttons = allspice_buttons_cache_get();
    allspice_console_log('[Allspice][PHP][floating] buttons_cache_count=' . (is_array($buttons) ? count($buttons) : 0));
    if (!$buttons || !is_array($buttons)) {
        allspice_console_log('[Allspice][PHP][floating] EXIT no buttons cache');
        return;
    }
    $rail_candidate = null;
    $standard_float_candidate = null;
    foreach ($buttons as $b) {
        if (empty($b['active'])) {
            continue;
        }
        $id = trim((string)($b['id'] ?? ''));
        if ($id === '') {
            continue;
        }
        if ((string)($b['placement'] ?? 'manual_only') !== 'automatic') {
            continue;
        }
        $style = strtolower((string)($b['style'] ?? ''));
        if (allspice_is_rail_button_id($id)) {
            if (!$has_manual_rail && $rail_candidate === null) {
                $rail_candidate = $b;
            }
            continue;
        }
        if ($style === 'floating') {
            if (!$has_manual_standard_float) {
                if ($id === 'WIDCON-ea5d') {
                    $standard_float_candidate = $b;
                } elseif ($standard_float_candidate === null) {
                    $standard_float_candidate = $b;
                }
            }
            continue;
        }
    }
    if ($rail_candidate && !allspice_floating_candidate_allowed_on_current_page($rail_candidate, $is_recipe)) {
        allspice_console_log('[Allspice][PHP][floating] rail skipped because non-recipe and show_on_all_pages disabled');
        $rail_candidate = null;
    }
    if ($standard_float_candidate && !allspice_floating_candidate_allowed_on_current_page($standard_float_candidate, $is_recipe)) {
        allspice_console_log('[Allspice][PHP][floating] standard float skipped because non-recipe and show_on_all_pages disabled');
        $standard_float_candidate = null;
    }
    allspice_console_log('[Allspice][PHP][floating] selected_rail_id=' . ($rail_candidate ? (string)($rail_candidate['id'] ?? 'NONE') : 'NONE'));
    allspice_console_log('[Allspice][PHP][floating] selected_standard_float_id=' . ($standard_float_candidate ? (string)($standard_float_candidate['id'] ?? 'NONE') : 'NONE'));
    if (!$rail_candidate && !$standard_float_candidate) {
        allspice_console_log('[Allspice][PHP][floating] EXIT no eligible floating surfaces');
        return;
    }
    if ($standard_float_candidate) {
        allspice_render_auto_floating_surface($standard_float_candidate, 'floating', $page_kind);
    }
    if ($rail_candidate) {
        allspice_render_auto_floating_surface($rail_candidate, 'rail', $page_kind);
    }
}

function allspice_render_auto_floating_surface(array $button, string $surface_kind, string $page_kind): void {
    $button_id = trim((string)($button['id'] ?? ''));
    if ($button_id === '') {
        return;
    }
    $is_rail = allspice_is_rail_button_id($button_id);
    $opts = (!empty($button['options']) && is_array($button['options']))
        ? $button['options']
        : array();
    $req_uri = allspice_get_request_uri();
    if ($is_rail) {
        $wrap_id = 'asx-railwrap-' . substr(md5($button_id . '|' . $req_uri), 0, 10);
        if (!empty($GLOBALS['allspice_auto_rail_injected']) && $GLOBALS['allspice_auto_rail_injected'] === true) {
            allspice_console_log('[Allspice][PHP][floating] EXIT rail already injected');
            return;
        }
        if (!empty($GLOBALS['allspice_auto_rail_wrap_id']) && $GLOBALS['allspice_auto_rail_wrap_id'] === $wrap_id) {
            allspice_console_log('[Allspice][PHP][floating] EXIT same rail wrap_id already used');
            return;
        }
        $GLOBALS['allspice_auto_rail_injected'] = true;
        $GLOBALS['allspice_auto_rail_wrap_id'] = $wrap_id;
    } else {
        $wrap_id = 'asx-floatwrap-' . substr(md5($button_id . '|' . $req_uri), 0, 10);
        if (!empty($GLOBALS['allspice_auto_float_injected']) && $GLOBALS['allspice_auto_float_injected'] === true) {
            allspice_console_log('[Allspice][PHP][floating] EXIT standard float already injected');
            return;
        }
        if (!empty($GLOBALS['allspice_auto_float_wrap_id']) && $GLOBALS['allspice_auto_float_wrap_id'] === $wrap_id) {
            allspice_console_log('[Allspice][PHP][floating] EXIT same standard float wrap_id already used');
            return;
        }
        $GLOBALS['allspice_auto_float_injected'] = true;
        $GLOBALS['allspice_auto_float_wrap_id'] = $wrap_id;
    }
    $float_side = 'right';
    $candidate_side = strtolower(trim((string)($opts['float_side'] ?? '')));
    if ($candidate_side === 'left' || $candidate_side === 'right') {
        $float_side = $candidate_side;
    }
    $initial_dock = ($float_side === 'left') ? 'ml' : 'mr';
    if ($is_rail) {
        $valid_docks = array('tl', 'tr', 'bl', 'br', 'ml', 'mr');
        $dock_left = array('tl', 'bl', 'ml');
        $dp = isset($opts['default_position'])
            ? strtolower(trim((string)$opts['default_position']))
            : '';
        if ($dp !== '' && in_array($dp, $valid_docks, true)) {
            $initial_dock = $dp;
            $float_side = in_array($dp, $dock_left, true) ? 'left' : 'right';
        }
    }
    $html = $is_rail
        ? allspice_render_slot_html($button_id, false, 1, 1)
        : allspice_render_slot_html($button_id, false, 220, 196);
    if ($is_rail) {
        $wrap_style = 'position:fixed;inset:0;width:auto;height:auto;max-width:none;margin:0;padding:0;overflow:visible;pointer-events:none;z-index:9000;';
    } else {
        $side_style = ($float_side === 'left')
            ? 'left:20px;'
            : 'right:20px;';
        $wrap_style = 'position:fixed;' . $side_style . 'bottom:20px;transform:translateY(calc(-1 * var(--asx-bottom-offset, 0px)));will-change:transform;z-index:9000;';
    }
    $wrap_class = $is_rail
        ? 'asx-floatwrap asx-railwrap'
        : 'asx-floatwrap asx-floatingwrap';
    $wrap_open = '<div id="' . esc_attr($wrap_id) . '" class="' . esc_attr($wrap_class) . '" data-asx-hide="1" data-asx-side="' . esc_attr($float_side) . '" data-asx-initial-dock="' . esc_attr($initial_dock) . '" data-asx-page-kind="' . esc_attr($page_kind) . '" data-asx-surface="' . esc_attr($surface_kind) . '" style="' . esc_attr($wrap_style) . '">';
    echo wp_kses($wrap_open, allspice_allowed_html());
    echo wp_kses($html, allspice_allowed_html());
    echo wp_kses('</div>', allspice_allowed_html());
    $event_payload = array(
        'kind' => $is_rail ? 'rail' : 'floating',
        'surface' => $surface_kind,
        'wrapId' => $wrap_id,
        'buttonId' => $button_id,
        'ts' => time(),
    );
    $js = '(() => { try {
        const detail = ' . wp_json_encode($event_payload, JSON_UNESCAPED_SLASHES) . ';
        window.__ALLSPICE_SLOT_QUEUE = window.__ALLSPICE_SLOT_QUEUE || [];
        window.__ALLSPICE_SLOT_QUEUE.push(detail);
        try { document.dispatchEvent(new CustomEvent("allspice:slotAdded", { detail })); }
        catch(e1){
          try { var ev=document.createEvent("CustomEvent"); ev.initCustomEvent("allspice:slotAdded", true, true, detail); document.dispatchEvent(ev); }
          catch(e2){}
        }
    } catch(e3){} })();';
    if (wp_script_is('allspice-widget-page', 'enqueued') || wp_script_is('allspice-widget-page', 'registered')) {
        wp_add_inline_script('allspice-widget-page', $js, 'after');
    } else {
        echo '<script>' . $js . '</script>';
    }
}

function allspice_normalize_placement(string $style, string $placement): string {
    $style = strtolower($style);
    $placement = strtolower($placement);
    if ($style === 'inline') {
        $ok = array(
            'manual_only',
            'top_of_post',
            'top_of_recipe_card',
            'above_recipe_card',
            'under_recipe_card',
            'end_of_post',
            'after_introduction',
            'after_first_section'
        );
        return in_array($placement, $ok, true) ? $placement : 'manual_only';
    }
    if ($style === 'floating') {
        $ok = array('manual_only','automatic');
        return in_array($placement, $ok, true) ? $placement : 'manual_only';
    }
    return 'manual_only';
}

function allspice_find_first_wprm_recipe_container_open_tag(string $content): ?array {
    if (!preg_match_all(
        '/<div\b[^>]*class=(["\'])(.*?)\1[^>]*>/is',
        $content,
        $matches,
        PREG_OFFSET_CAPTURE
    )) {
        return null;
    }
    foreach ($matches[0] as $i => $fullMatch) {
        $fullTag = $fullMatch[0];
        $fullPos = $fullMatch[1];
        $classValue = $matches[2][$i][0] ?? '';
        $classes = preg_split('/\s+/', trim((string)$classValue));
        if (!$classes || !is_array($classes)) {
            continue;
        }
        if (in_array('wprm-recipe-container', $classes, true)) {
            return array(
                'tag'   => $fullTag,
                'start' => $fullPos,
                'end'   => $fullPos + strlen($fullTag),
            );
        }
    }
    return null;
}

function allspice_find_first_wprm_recipe_open_tag(string $content): ?array {
    if (!preg_match_all('/<div\b[^>]*class=(["\'])(.*?)\1[^>]*>/is', $content, $matches, PREG_OFFSET_CAPTURE)) {
        return null;
    }
    foreach ($matches[0] as $i => $fullMatch) {
        $fullTag = $fullMatch[0];
        $fullPos = $fullMatch[1];
        $classValue = $matches[2][$i][0] ?? '';
        $classes = preg_split('/\s+/', trim((string)$classValue));
        if (!$classes || !is_array($classes)) continue;
        $hasRecipe = in_array('wprm-recipe', $classes, true);
        $hasContainer = in_array('wprm-recipe-container', $classes, true);
        if ($hasRecipe && !$hasContainer) {
            return array(
                'tag' => $fullTag,
                'start' => $fullPos,
                'end' => $fullPos + strlen($fullTag),
            );
        }
    }
    return null;
}

function allspice_find_matching_div_end(string $content, int $openTagStart): ?int {
    if (!preg_match('/<div\b[^>]*>/is', $content, $openMatch, PREG_OFFSET_CAPTURE, $openTagStart)) {
        return null;
    }
    if ((int)$openMatch[0][1] !== $openTagStart) {
        return null;
    }
    $cursor = $openTagStart;
    $depth = 0;
    $len = strlen($content);
    while ($cursor < $len && preg_match('/<\/?div\b[^>]*>/is', $content, $m, PREG_OFFSET_CAPTURE, $cursor)) {
        $tag = $m[0][0];
        $pos = (int)$m[0][1];
        if (preg_match('/^<div\b/is', $tag)) {
            $depth++;
        } else {
            $depth--;
            if ($depth === 0) {
                return $pos + strlen($tag);
            }
        }
        $cursor = $pos + strlen($tag);
    }
    return null;
}

function allspice_find_wprm_recipe_bounds(string $content): ?array {
    $open = allspice_find_first_wprm_recipe_open_tag($content);
    if (!$open) return null;

    $closeEnd = allspice_find_matching_div_end($content, (int)$open['start']);
    if ($closeEnd === null) return null;

    return array(
        'start' => (int)$open['start'],
        'open_end' => (int)$open['end'],
        'end' => (int)$closeEnd,
    );
}

function allspice_post_manual_button_ids($postObj): array {
    if (!$postObj || empty($postObj->post_content)) return array();
    $c = (string)$postObj->post_content;
    if (!preg_match_all('/allspice_button[^\]]*id\s*=\s*"([^"]+)"/i', $c, $m1)) $m1 = array(array(), array());
    if (!preg_match_all('/data-allspice-button\s*=\s*"([^"]+)"/i', $c, $m2)) $m2 = array(array(), array());
    if (!preg_match_all('/data-allspice-id\s*=\s*"([^"]+)"/i', $c, $m3)) $m3 = array(array(), array());
    $ids = array_merge($m1[1] ?? array(), $m2[1] ?? array(), $m3[1] ?? array());
    $ids = array_map('trim', $ids);
    $ids = array_filter($ids);
    return array_values(array_unique($ids));
}

function allspice_cached_button_map_by_id(): array {
    $buttons = allspice_buttons_cache_get();
    $map = array();
    foreach ($buttons as $b) {
        $id = trim((string)($b['id'] ?? ''));
        if ($id !== '') $map[$id] = $b;
    }
    return $map;
}

function allspice_has_manual_inline_placements_in_post($postObj = null): bool {
    if (!$postObj) { global $post; $postObj = $post; }
    if (!$postObj) return false;
    $ids = allspice_post_manual_button_ids($postObj);
    if (!$ids) return false;
    $map = allspice_cached_button_map_by_id();
    foreach ($ids as $id) {
        $b = $map[$id] ?? null;
        if (!$b) continue;
        $style = strtolower((string)($b['style'] ?? ''));
        if ($style === 'inline') return true;
    }
    if (function_exists('has_block')) {
        if (
            has_block('allspice/inline-chat', $postObj) ||
            has_block('allspice/inline-search', $postObj) ||
            has_block('allspice/inline-overview-style-1', $postObj) ||
            has_block('allspice/inline-overview-style-2', $postObj) ||
            has_block('allspice/inline-cooking-cta', $postObj)
        ) return true;
    }
    return false;
}

function allspice_has_manual_rail_placements_in_post($postObj = null): bool {
    if (!$postObj) {
        global $post;
        $postObj = $post;
    }
    if (!$postObj) {
        return false;
    }
    $ids = allspice_post_manual_button_ids($postObj);
    if ($ids) {
        foreach ($ids as $id) {
            if (allspice_is_rail_button_id((string)$id)) {
                return true;
            }
        }
    }
    if (function_exists('has_block')) {
        if (has_block('allspice/rail', $postObj)) {
            return true;
        }
    }
    return false;
}

function allspice_has_manual_standard_floating_placements_in_post($postObj = null): bool {
    if (!$postObj) {
        global $post;
        $postObj = $post;
    }
    if (!$postObj) {
        return false;
    }
    $ids = allspice_post_manual_button_ids($postObj);
    if ($ids) {
        $map = allspice_cached_button_map_by_id();
        foreach ($ids as $id) {
            $id = trim((string)$id);
            if ($id === '') {
                continue;
            }
            if (allspice_is_rail_button_id($id)) {
                continue;
            }
            $b = $map[$id] ?? null;
            if (!$b) {
                continue;
            }
            if (allspice_is_standard_floating_button($b)) {
                return true;
            }
        }
    }
    if (function_exists('has_block')) {
        if (has_block('allspice/floating-button', $postObj)) {
            return true;
        }
    }
    return false;
}

function allspice_has_manual_floating_placements_in_post($postObj = null): bool {
    return allspice_has_manual_rail_placements_in_post($postObj) || allspice_has_manual_standard_floating_placements_in_post($postObj);
}

function allspice_enqueue_widget_files(): void {
    allspice_console_log('[Allspice] enqueue_widget_files needed=' . (!empty($GLOBALS['allspice_widget_needed']) ? '1' : '0'));
    if (!defined('ALLSPICE_PAGE_BUNDLE_BASE')) {
        allspice_console_log('[Allspice] missing page bundle env config');
        return;
    }
    if (is_admin() || !allspice_widget_needed_now()) return;
    allspice_console_log('[Allspice] enqueue_widget_files passed first guard.');
    $ver = allspice_assets_version();
    $base = trim((string) ALLSPICE_PAGE_BUNDLE_BASE);
    $plugin_ver = defined('ALLSPICE_PLUGIN_VERSION')
        ? (string) ALLSPICE_PLUGIN_VERSION
        : allspice_get_plugin_version();
    $plugin_ver = preg_replace('/[^0-9.]/', '', $plugin_ver);
    if ($base === '' || $plugin_ver === '') {
        allspice_console_log('[Allspice] invalid page bundle config', [
            'base' => $base,
            'plugin_ver' => $plugin_ver,
        ]);
        return;
    }
    $entry_file = defined('ALLSPICE_PAGE_ENTRY_FILE')
        ? trim((string) ALLSPICE_PAGE_ENTRY_FILE)
        : 'main.js';
    $chunks_dir = defined('ALLSPICE_PAGE_CHUNKS_DIR')
        ? trim((string) ALLSPICE_PAGE_CHUNKS_DIR)
        : 'chunks/';
    $page_version_base = trailingslashit(trailingslashit($base) . $plugin_ver);
    $page_src = trailingslashit($page_version_base) . $entry_file;
    $page_chunk_base = trailingslashit($page_version_base) . trailingslashit($chunks_dir);
    wp_register_script('allspice-widget-page', $page_src, array(), $ver, true);
    wp_enqueue_script('allspice-widget-page');
    wp_register_style('allspice-compat', false, [], $ver);
    wp_enqueue_style('allspice-compat');
    wp_add_inline_style('allspice-compat',
        '@media only screen and (max-width:450px){body:has(#allspicewidget) #universalPlayer_wrapper{align-self:end;left:-15px;bottom:60px}}'
    );
    $widget_base = allspice_get_widget_asset_base();
    $css_url = trailingslashit($widget_base) .
        'allspicewidget-' . $plugin_ver . '.css';
    $umd_url = trailingslashit($widget_base) .
        'allspicewidget-' . $plugin_ver . '.umd.cjs';
    wp_add_inline_script(
        'allspice-widget-page',
        '/* nowprocket */ ' . allspice_inline_lazy_asset_loader_js(
            $css_url,
            $umd_url,
            allspice_should_lazy_assets_until_open()
        ),
        'before'
    );
    $s = allspice_opt_get();
    $payload = array(
        'partner_id' => (string)$s['partner_id'],
        'domain_id' => (string)$s['domain_id'],
        'fetched_at' => (int)$s['cache_fetched_at'],
        'count' => (int)$s['buttons_count'],
        'buttons' => allspice_buttons_cache_get(),
    );
    allspice_console_log('[Allspice] adding inline cache scripts for handle allspice-widget-page', [
        'ver' => $ver,
        'page_src' => $page_src,
        'count' => $payload['count'] ?? null,
    ]);
    wp_add_inline_script(
        'allspice-widget-page',
        '/* nowprocket */ window.__ALLSPICE_DEBUG=' . (allspice_is_debug() ? 'true' : 'false') . ';',
        'before'
    );
    wp_add_inline_script(
        'allspice-widget-page',
        '/* nowprocket */ window.__ALLSPICE_VERSION=' . wp_json_encode($plugin_ver) . ';
         window.__ALLSPICE_PAGE_BASE=' . wp_json_encode(trailingslashit($page_version_base)) . ';
         window.__ALLSPICE_PAGE_CHUNK_BASE=' . wp_json_encode($page_chunk_base) . ';',
        'before'
    );
    wp_add_inline_script(
        'allspice-widget-page',
        '/* nowprocket */ (() => { 
            try {
              window.__ALLSPICE_BUTTONS_CACHE = ' . wp_json_encode($payload) . ';
              window.__ALLSPICE_CACHE_READY = window.__ALLSPICE_CACHE_READY || {};
              window.__ALLSPICE_CACHE_READY.buttons = true;
              document.dispatchEvent(new CustomEvent("allspice:cacheReady", { detail: { kind: "buttons" } }));
            } catch(e) {}
          })();',
        'before'
    );
    $tp_payload = allspice_theme_policy_payload();
    wp_add_inline_script(
        'allspice-widget-page',
        '/* nowprocket */ (() => { 
            try {
              window.__ALLSPICE_THEME_POLICY_CACHE = ' . wp_json_encode($tp_payload) . ';
              window.__ALLSPICE_CACHE_READY.theme_policy = true;
              document.dispatchEvent(new CustomEvent("allspice:cacheReady", { detail: { kind: "theme_policy" } }));
            } catch(e) {}
          })();',
        'before'
    );
    $is_recipe = allspice_is_recipe_page();
    $uk = null;
    $rid = null;
    $overview = null;
    if ($is_recipe) {
        $uk = allspice_current_url_key();
        if (is_string($uk) && $uk !== '') {
            $ctx_row = function_exists('allspice_recipes_get_overview_by_url_key')
                ? allspice_recipes_get_overview_by_url_key($uk)
                : null;
            if (is_array($ctx_row) || is_object($ctx_row)) {
                if (is_object($ctx_row)) $ctx_row = (array) $ctx_row;
                $rid = !empty($ctx_row['recipe_id']) ? (string)$ctx_row['recipe_id'] : null;
                $overview = [
                    'url' => $ctx_row['url'] ?? null,
                    'wp_post_id' => $ctx_row['wp_post_id'] ?? null,
                    'time' => $ctx_row['time'] ?? null,
                    'meta_description' => $ctx_row['meta_description'] ?? null,
                    'meta_description_valid' => array_key_exists('meta_description_valid', $ctx_row) ? $ctx_row['meta_description_valid'] : null,
                    'takeaways' => is_array($ctx_row['takeaways'] ?? null) ? $ctx_row['takeaways'] : [],
                    'faqs' => is_array($ctx_row['faqs'] ?? null) ? $ctx_row['faqs'] : [],
                    'recipe_questions' => is_array($ctx_row['recipe_questions'] ?? null) ? $ctx_row['recipe_questions'] : [],
                    'servings' => $ctx_row['servings'] ?? null,
                    'calories' => $ctx_row['calories'] ?? null,
                    'recipe_title' => $ctx_row['recipe_title'] ?? null,
                    'ingredients' => is_array($ctx_row['ingredients'] ?? null) ? $ctx_row['ingredients'] : [],
                ];
            } else {
                $rid = allspice_recipes_get_id_by_url_key($uk);
                if (!$rid) $rid = null;
            }
        } else {
            $uk = null;
        }
    }
    wp_add_inline_script(
        'allspice-widget-page',
        '/* nowprocket */ window.__ALLSPICE_RECIPE_CTX=' . wp_json_encode([
            'is_recipe' => (bool)$is_recipe,
            'url_key'   => $uk,
            'recipe_id' => $rid,
            'overview'  => $overview,
        ]) . ';',
        'before'
    );
}

add_action('wp_enqueue_scripts', 'allspice_enqueue_widget_files');

function allspice_should_lazy_assets_until_open(): bool {
    return true;
}

function allspice_enqueue_bootstrap(): void {
    if (is_admin()) return;
    $ver = allspice_assets_version();
    $src = ALLSPICE_WIDGET_LOADER_URL . 'dist/asx-bootstrap.js';
    wp_register_script('allspice-bootstrap', $src, array(), $ver, true);
    wp_enqueue_script('allspice-bootstrap');
    if (!defined('ALLSPICE_PAGE_BUNDLE_BASE')) {
        allspice_console_log('[Allspice] missing page bundle env config');
        return;
    }
    $base = trim((string) ALLSPICE_PAGE_BUNDLE_BASE);
    $plugin_ver = defined('ALLSPICE_PLUGIN_VERSION')
        ? (string) ALLSPICE_PLUGIN_VERSION
        : allspice_get_plugin_version();
    $plugin_ver = preg_replace('/[^0-9.]/', '', $plugin_ver);
    if ($plugin_ver === '') return;
    $entry_file = defined('ALLSPICE_PAGE_ENTRY_FILE')
        ? trim((string) ALLSPICE_PAGE_ENTRY_FILE)
        : 'main.js';
    $chunks_dir = defined('ALLSPICE_PAGE_CHUNKS_DIR')
        ? trim((string) ALLSPICE_PAGE_CHUNKS_DIR)
        : 'chunks/';
    $page_version_base = trailingslashit(trailingslashit($base) . $plugin_ver);
    $page_src = trailingslashit($page_version_base) . $entry_file;
    $page_chunk_base = trailingslashit($page_version_base) . trailingslashit($chunks_dir);
    $widget_base = allspice_get_widget_asset_base();
    $css_url = trailingslashit($widget_base) .
        'allspicewidget-' . $plugin_ver . '.css';
    $umd_url = trailingslashit($widget_base) .
        'allspicewidget-' . $plugin_ver . '.umd.cjs';
    $s = allspice_opt_get();
    $buttons_payload = array(
        'partner_id' => (string)($s['partner_id'] ?? ''),
        'domain_id' => (string)($s['domain_id'] ?? ''),
        'fetched_at' => (int)($s['cache_fetched_at'] ?? 0),
        'count' => (int)($s['buttons_count'] ?? 0),
        'buttons' => allspice_buttons_cache_get(),
    );
    $tp_payload = allspice_theme_policy_payload();
    $is_recipe = allspice_is_recipe_page();
    $recipe_ctx = array('is_recipe' => (bool)$is_recipe, 'url_key' => null, 'recipe_id' => null, 'overview' => null);
    if ($is_recipe) {
        $uk = allspice_current_url_key();
        $rid = null;
        $overview = null;
        if (is_string($uk) && $uk !== '') {
            $ctx_row = function_exists('allspice_recipes_get_overview_by_url_key')
                ? allspice_recipes_get_overview_by_url_key($uk)
                : null;
            if (is_array($ctx_row) || is_object($ctx_row)) {
                if (is_object($ctx_row)) $ctx_row = (array)$ctx_row;
                $rid = !empty($ctx_row['recipe_id']) ? (string)$ctx_row['recipe_id'] : null;
                $overview = array(
                    'url' => $ctx_row['url'] ?? null,
                    'wp_post_id' => $ctx_row['wp_post_id'] ?? null,
                    'time' => $ctx_row['time'] ?? null,
                    'meta_description' => $ctx_row['meta_description'] ?? null,
                    'meta_description_valid' => array_key_exists('meta_description_valid', $ctx_row) ? $ctx_row['meta_description_valid'] : null,
                    'takeaways' => is_array($ctx_row['takeaways'] ?? null) ? $ctx_row['takeaways'] : array(),
                    'faqs' => is_array($ctx_row['faqs'] ?? null) ? $ctx_row['faqs'] : array(),
                    'recipe_questions' => is_array($ctx_row['recipe_questions'] ?? null) ? $ctx_row['recipe_questions'] : [],
                    'servings' => $ctx_row['servings'] ?? null,
                    'calories' => $ctx_row['calories'] ?? null,
                    'recipe_title' => $ctx_row['recipe_title'] ?? null,
                    'ingredients' => is_array($ctx_row['ingredients'] ?? null) ? $ctx_row['ingredients'] : array(),
                );
            } else if (function_exists('allspice_recipes_get_id_by_url_key')) {
                $rid = allspice_recipes_get_id_by_url_key($uk);
                if (!$rid) $rid = null;
            }
            $recipe_ctx = array('is_recipe' => true, 'url_key' => $uk, 'recipe_id' => $rid, 'overview' => $overview);
        }
    }
    $lazy_assets_until_open = allspice_should_lazy_assets_until_open();
    $widget_variant = allspice_get_widget_variant();
    $cfg = array(
        'debug' => allspice_is_debug(),
        'version' => $plugin_ver,
        'widget_variant' => $widget_variant,
        'page_src' => $page_src,
        'page_base' => trailingslashit($page_version_base),
        'page_chunk_base' => $page_chunk_base,
        'css_url' => $css_url,
        'umd_url' => $umd_url,
        'buttons_cache' => $buttons_payload,
        'theme_policy' => $tp_payload,
        'recipe_ctx' => $recipe_ctx,
        'lazy_assets_until_open' => $lazy_assets_until_open,
        'float_auto_open_enabled' => $widget_variant === 'float',
        'float_auto_open_delay_ms' => 30000,
        'float_auto_open_desktop_only' => true,
    );
    /*
     * Float launcher appearance. Merged in only for the float variant so standard sites emit a
     * byte-identical config to before — nothing downstream can branch on keys that are not there.
     *
     * Note float_launcher_side was already being READ by main.js (asxGetFloatLauncherCandidate)
     * but never emitted here, so every float site was pinned to the right edge regardless of intent.
     */
    if ($widget_variant === 'float') {
        $cfg = array_merge($cfg, allspice_get_float_launcher_options());
    }
    wp_add_inline_script(
        'allspice-bootstrap',
        'window.__ALLSPICE_BOOTSTRAP_CFG=' . wp_json_encode($cfg, JSON_UNESCAPED_SLASHES) . ';',
        'before'
    );
}
add_action('wp_enqueue_scripts', 'allspice_enqueue_bootstrap', 1);

function allspice_inline_lazy_asset_loader_js(string $css_url, string $umd_url, bool $lazy_assets_until_open = false): string {
    $lazy_assets_json = $lazy_assets_until_open ? 'true' : 'false';
    $css_json = wp_json_encode($css_url, JSON_UNESCAPED_SLASHES);
    $umd_json = wp_json_encode($umd_url, JSON_UNESCAPED_SLASHES);
    $js  = '(function(){';
    $js .= 'var CSS_URL=' . $css_json . ';';
    $js .= 'var UMD_URL=' . $umd_json . ';';
    $js .= 'var LOAD_TIMEOUT_MS=8000;';
    $js .= 'var LAZY_ASSETS_UNTIL_OPEN=' . $lazy_assets_json . ';';
    $js .= 'if (window.__ALLSPICE_LOAD_ASSETS) { return; }';
    $js .= 'var cssPromise=null;';
    $js .= 'var umdPromise=null;';
    $js .= 'function loadCss(){';
    $js .= '  if (cssPromise) return cssPromise;';
    $js .= '  cssPromise=new Promise(function(resolve){';
    $js .= '    if (!CSS_URL) { resolve(false); return; }';
    $js .= '    try{';
    $js .= '      var existing=document.querySelector(\'link[data-allspice-css="1"]\');';
    $js .= '      if(existing){ resolve(true); return; }';
    $js .= '      var done=false;';
    $js .= '      function finish(ok){ if(done) return; done=true; resolve(ok); }';
    $js .= '      var l=document.createElement("link");';
    $js .= '      var t=setTimeout(function(){ finish(false); }, LOAD_TIMEOUT_MS);';
    $js .= '      l.rel="stylesheet";';
    $js .= '      l.href=CSS_URL;';
    $js .= '      l.setAttribute("data-allspice-css","1");';
    $js .= '      l.onload=function(){';
    $js .= '        try{ clearTimeout(t); }catch(e){}';
    $js .= '        try{document.dispatchEvent(new CustomEvent("allspice:cssReady"));}catch(e2){}';
    $js .= '        finish(true);';
    $js .= '      };';
    $js .= '      l.onerror=function(){';
    $js .= '        try{ clearTimeout(t); }catch(e){}';
    $js .= '        finish(false);';
    $js .= '      };';
    $js .= '      (document.head||document.documentElement).appendChild(l);';
    $js .= '    }catch(e3){ resolve(false); }';
    $js .= '  });';
    $js .= '  return cssPromise;';
    $js .= '}';
    $js .= 'function loadUmd(){';
    $js .= '  if (umdPromise) return umdPromise;';
    $js .= '  umdPromise=new Promise(function(resolve){';
    $js .= '    if (!UMD_URL) { resolve(false); return; }';
    $js .= '    try{';
    $js .= '      var existing=document.querySelector(\'script[data-allspice-umd="1"]\');';
    $js .= '      if(existing){ resolve(true); return; }';
    $js .= '      var done=false;';
    $js .= '      function finish(ok){ if(done) return; done=true; resolve(ok); }';
    $js .= '      var s=document.createElement("script");';
    $js .= '      var t=setTimeout(function(){ finish(false); }, LOAD_TIMEOUT_MS);';
    $js .= '      s.src=UMD_URL;';
    $js .= '      s.async=true;';
    $js .= '      s.setAttribute("data-allspice-umd","1");';
    $js .= '      s.onload=function(){';
    $js .= '        try{ clearTimeout(t); }catch(e){}';
    $js .= '        try{document.dispatchEvent(new CustomEvent("allspice:umdReady"));}catch(e2){}';
    $js .= '        finish(true);';
    $js .= '      };';
    $js .= '      s.onerror=function(){';
    $js .= '        try{ clearTimeout(t); }catch(e){}';
    $js .= '        finish(false);';
    $js .= '      };';
    $js .= '      (document.head||document.documentElement).appendChild(s);';
    $js .= '    }catch(e3){ resolve(false); }';
    $js .= '  });';
    $js .= '  return umdPromise;';
    $js .= '}';
    $js .= 'function loadAssets(){';
    $js .= '  if (LAZY_ASSETS_UNTIL_OPEN) {';
    $js .= '    return Promise.resolve({ css: false, umd: false, deferred: true });';
    $js .= '  }';
    $js .= '  return Promise.all([loadCss(), loadUmd()]).then(function(res){';
    $js .= '    return { css: !!res[0], umd: !!res[1] };';
    $js .= '  });';
    $js .= '}';
    $js .= 'function loadOpenAssets(){';
    $js .= '  return Promise.all([loadCss(), loadUmd()]).then(function(res){';
    $js .= '    return { css: !!res[0], umd: !!res[1] };';
    $js .= '  });';
    $js .= '}';
    $js .= 'window.__ALLSPICE_LOAD_CSS = loadCss;';
    $js .= 'window.__ALLSPICE_LOAD_UMD = loadUmd;';
    $js .= 'window.__ALLSPICE_LOAD_ASSETS = loadAssets;';
    $js .= 'window.__ALLSPICE_LOAD_OPEN_ASSETS = loadOpenAssets;';
    $js .= 'function armOnce(){';
    $js .= '  var fired=false;';
    $js .= '  function onFirst(){';
    $js .= '    if(fired) return; fired=true;';
    $js .= '    try{window.removeEventListener("pointerdown",onFirst,true);}catch(e){}';
    $js .= '    try{window.removeEventListener("touchstart",onFirst,true);}catch(e){}';
    $js .= '    try{window.removeEventListener("keydown",onFirst,true);}catch(e){}';
    $js .= '    try{window.removeEventListener("scroll",onFirst,true);}catch(e){}';
    $js .= '    loadAssets();';
    $js .= '  }';
    $js .= '  try{window.addEventListener("pointerdown",onFirst,true);}catch(e){}';
    $js .= '  try{window.addEventListener("touchstart",onFirst,true);}catch(e){}';
    $js .= '  try{window.addEventListener("keydown",onFirst,true);}catch(e){}';
    $js .= '  try{window.addEventListener("scroll",onFirst,true);}catch(e){}';
    $js .= '}';
    $js .= 'function isHostedAuthReturn(){';
    $js .= '  try {';
    $js .= '    var hash = String(window.location.hash || "");';
    $js .= '    if (hash.indexOf("asx_auth_code=") !== -1) return true;';
    $js .= '    if (hash.indexOf("asx_auth_provider=") !== -1) return true;';
    $js .= '    if (hash.indexOf("signInSource=hosted_oauth") !== -1) return true;';
    $js .= '    if (window.localStorage && localStorage.getItem("allspiceHostedAuthState")) return true;';
    $js .= '    if (window.sessionStorage) {';
    $js .= '      var rawRestore = sessionStorage.getItem("allspiceHostedAuthUIRestore");';
    $js .= '      if (rawRestore) {';
    $js .= '        try {';
    $js .= '          var restore = JSON.parse(rawRestore);';
    $js .= '          if (restore && restore.savedAt && Date.now() - restore.savedAt < 10 * 60 * 1000) return true;';
    $js .= '        } catch(e2) {}';
    $js .= '      }';
    $js .= '    }';
    $js .= '  } catch(e) {}';
    $js .= '  return false;';
    $js .= '}';
    $js .= 'if (isHostedAuthReturn()) {';
    $js .= '  try { console.info("[Allspice Auth Bridge] Hosted auth return detected; loading widget assets immediately."); } catch(e) {}';
    $js .= '  loadOpenAssets();';
    $js .= '} else {';
    $js .= '  armOnce();';
    $js .= '  try{ setTimeout(function(){ loadAssets(); }, 5000); }catch(e){}';
    $js .= '}';
    $js .= '})();';
    return $js;
}