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/includes/settings.php
<?php
// includes/settings.php
if (!defined('ABSPATH')) exit;

define('ALLSPICE_SETTINGS_PAGE', 'allspice-settings');
define('ALLSPICE_SETTINGS_GROUP', 'allspice_settings_group');
define('ALLSPICE_SYNC_BUTTONS_SETTINGS_ACTION', 'allspice_sync_buttons_settings');
define('ALLSPICE_SYNC_RECIPES_ACTION', 'allspice_sync_recipes');
define('ALLSPICE_SYNC_ALL_ACTION', 'allspice_sync_all');

$css = '
.allspice-admin{
  --asx-solid:#F9705F;
  --asx-g1:#F25B57;
  --asx-g2:#FF8266;
  --asx-text:#303030;
  --asx-text2:#606060;
  --asx-text3:#B0B0B0;
  --asx-bg:#FFFFFF;
  --asx-bg2:#F2F2F2;
  --asx-tint:#EAEAEA;
  --asx-blue:#0099FF;
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
}
.allspice-admin .description{ color: var(--asx-text2); }
.allspice-admin__stack{
  max-width: 980px;
  margin: 14px 0 18px 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.allspice-admin__card{
  background: var(--asx-bg);
  border: 1px solid rgba(194,194,194,0.38);
  border-radius: 14px;
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}
.allspice-admin__accent{
  height: 6px;
  background: linear-gradient(90deg, var(--asx-g1), var(--asx-g2));
}
.allspice-admin__body{ padding: 18px; }
.allspice-admin__sectionTitle{
  margin: 0 0 10px 0;
  color: var(--asx-text);
  font-weight: 900;
  font-size: 14px;
  letter-spacing: 0.2px;
}
.allspice-admin__kicker{
  margin: 0 0 10px 0;
  color: var(--asx-text3);
  font-weight: 900;
  letter-spacing: 0.10em;
  font-size: 11px;
}
.allspice-admin__divider{
  height: 1px;
  background: rgba(234,234,234,0.95);
  margin: 16px 0;
}
.allspice-admin .form-table th{
  color: var(--asx-text);
  font-weight: 900;
  padding-top: 12px;
}
.allspice-admin .form-table td{
  color: var(--asx-text2);
  padding-top: 12px;
}
.allspice-admin .form-table .description{
  margin: 6px 0 0 0;
  font-size: 12.5px;
  line-height: 1.45;
}
.allspice-admin input.regular-text,
.allspice-admin input.large-text,
.allspice-admin input[type="text"],
.allspice-admin input[type="password"]{
  border: 1px solid rgba(194,194,194,0.55);
  border-radius: 10px;
  padding: 8px 10px;
  box-shadow: none;
}
.allspice-admin input:focus{
  border-color: rgba(249,112,95,0.70);
  box-shadow: 0 0 0 3px rgba(249,112,95,0.18);
}
.allspice-admin .button,
.allspice-admin .button-secondary,
.allspice-admin .button-primary{
  border-radius: 10px !important;
  font-weight: 900 !important;
}
.allspice-admin .button-primary{
  background: linear-gradient(90deg, var(--asx-g1), var(--asx-g2)) !important;
  border: 0 !important;
  box-shadow: 0 6px 14px rgba(242,91,87,0.20) !important;
  text-shadow: none !important;
}
.allspice-admin .button-primary:hover{ filter: brightness(0.98); }
.allspice-admin .button-secondary{
  border-color: rgba(194,194,194,0.55) !important;
}
.allspice-admin .button:focus,
.allspice-admin .button-primary:focus,
.allspice-admin .button-secondary:focus{
  box-shadow: 0 0 0 3px rgba(249,112,95,0.18) !important;
  border-color: rgba(249,112,95,0.70) !important;
}
.allspice-admin__card .notice,
.allspice-admin__card .updated,
.allspice-admin__card .error,
.allspice-admin__card .wrap > .notice,
.allspice-admin__card .wrap > .updated,
.allspice-admin__card .wrap > .error{
  display:none !important;
}
.allspice-mkt{ max-width: 980px; margin: 0; }
.allspice-mkt__card{
  background: var(--asx-bg);
  border: 1px solid rgba(194,194,194,0.38);
  border-radius: 14px;
  overflow: hidden;
  box-shadow: 0 2px 10px rgba(0,0,0,0.06);
}
.allspice-mkt__accent{ height: 6px; background: linear-gradient(90deg, var(--asx-g1), var(--asx-g2)); }
.allspice-mkt__head{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:14px;
  padding: 14px 18px 0 18px;
}
.allspice-mkt__brand{
  display:flex;
  align-items:center;
  gap:12px;
  min-width: 0;
}
.allspice-mkt__brand img{ height: 28px; width:auto; display:block; }
.allspice-mkt__tag{
  color: var(--asx-text2);
  font-size: 12.5px;
  line-height: 1.3;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.allspice-mkt__ctaTop{
  display:flex;
  gap:10px;
  flex-wrap:wrap;
  justify-content:flex-end;
}
.allspice-mkt__btnPrimary{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  padding: 9px 12px;
  border-radius: 10px;
  border: 0;
  text-decoration:none;
  color:#fff !important;
  background: linear-gradient(90deg, var(--asx-g1), var(--asx-g2));
  font-weight: 900;
  font-size: 13px;
  box-shadow: 0 6px 14px rgba(242,91,87,0.22);
}
.allspice-mkt__btnPrimary:hover{ filter: brightness(0.98); transform: translateY(-0.5px); }
.allspice-mkt__btnSecondary{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  padding: 9px 12px;
  border-radius: 10px;
  border: 1px solid rgba(234,234,234,0.95);
  text-decoration:none;
  color: var(--asx-text) !important;
  background:#fff;
  font-weight: 900;
  font-size: 13px;
}
.allspice-mkt__btnSecondary:hover{ border-color: rgba(249,112,95,0.55); color: var(--asx-solid) !important; }
.allspice-mkt__inner{
  display: grid;
  grid-template-columns: 1.25fr 0.75fr;
  gap: 18px;
  padding: 18px;
}
@media (max-width: 900px){ .allspice-mkt__inner{ grid-template-columns: 1fr; } }
.allspice-mkt__leftTitle{
  margin:0;
  color: var(--asx-text3);
  font-weight: 900;
  letter-spacing: 0.10em;
  font-size: 11px;
}
.allspice-mkt__grid{
  margin-top: 10px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
@media (max-width: 900px){ .allspice-mkt__grid{ grid-template-columns: 1fr; } }
.allspice-mkt__feature{
  background: rgba(242,242,242,0.55);
  border: 1px solid rgba(234,234,234,0.95);
  border-radius: 12px;
  padding: 10px 12px;
}
.allspice-mkt__featureTitle{
  margin:0;
  color: var(--asx-text);
  font-weight: 900;
  font-size: 13px;
  letter-spacing: 0.2px;
}
.allspice-mkt__featureBody{
  margin: 4px 0 0 0;
  color: var(--asx-text2);
  font-size: 13px;
  line-height: 1.55;
}
.allspice-mkt__right{
  border-left: 1px solid rgba(234,234,234,0.9);
  padding-left: 18px;
}
@media (max-width: 900px){
  .allspice-mkt__right{
    border-left: none;
    padding-left: 0;
    border-top: 1px solid rgba(234,234,234,0.9);
    padding-top: 14px;
  }
}
.allspice-mkt__stepsTitle{
  margin:0;
  color: var(--asx-text3);
  font-weight: 900;
  letter-spacing: 0.10em;
  font-size: 11px;
}
.allspice-mkt__steps{
  margin: 10px 0 0 0;
  padding-left: 18px;
  color: var(--asx-text2);
  font-size: 13px;
  line-height: 1.55;
}
.allspice-mkt__steps li{ margin: 6px 0; }
.allspice-mkt__steps strong{ color: var(--asx-text); font-weight: 900; }
.allspice-mkt__note{
  margin-top: 10px;
  padding: 10px 12px;
  background: rgba(242,242,242,0.60);
  border: 1px solid rgba(234,234,234,0.95);
  border-radius: 12px;
  color: var(--asx-text2);
  font-size: 12.5px;
  line-height: 1.5;
}
.allspice-mkt__note a{ color: var(--asx-blue); text-decoration:none; font-weight:900; }
.allspice-mkt__note a:hover{ text-decoration: underline; }
.allspice-premium-banner{
  margin: 16px 0 0 0;
  padding: 14px 16px;
  border-radius: 14px;
  border: 1px solid rgba(249,112,95,0.35);
  background: rgba(249,112,95,0.08);
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:14px;
}
.allspice-premium-banner__title{
  margin:0;
  color: var(--asx-text);
  font-weight:900;
  font-size:14px;
}
.allspice-premium-banner__body{
  margin:4px 0 0 0;
  color: var(--asx-text2);
  font-size:13px;
  line-height:1.45;
  max-width:700px;
}
.allspice-premium-banner__button{
  display:inline-flex;
  align-items:center;
  justify-content:center;
  white-space:nowrap;
  padding:9px 12px;
  border-radius:10px;
  background:linear-gradient(90deg, var(--asx-g1), var(--asx-g2));
  color:#fff !important;
  text-decoration:none;
  font-weight:900;
  box-shadow:0 6px 14px rgba(242,91,87,0.18);
}
.allspice-premium-banner__button:hover{
  filter:brightness(0.98);
}
@media (max-width: 700px){
  .allspice-premium-banner{
    align-items:flex-start;
    flex-direction:column;
  }
}
.allspice-sync-actions{
  display:flex;
  flex-wrap:wrap;
  gap:10px;
  align-items:center;
  margin-top:12px;
}
.allspice-sync-actions form{
  margin:0;
}
';

add_action('admin_enqueue_scripts', function ($hook) use ($css) {
    if ($hook !== 'settings_page_' . ALLSPICE_SETTINGS_PAGE) return;
    wp_register_style('allspice-admin-settings', false, [], '1.0.0');
    wp_enqueue_style('allspice-admin-settings');
    wp_add_inline_style('allspice-admin-settings', $css);
});

add_action('current_screen', function ($screen) {
    if (!$screen || $screen->id !== 'settings_page_' . ALLSPICE_SETTINGS_PAGE) return;
    remove_all_actions('admin_notices');
    remove_all_actions('all_admin_notices');
    remove_all_actions('network_admin_notices');
    remove_all_actions('user_admin_notices');
    add_action('admin_notices', function () {
        $n = get_transient('allspice_admin_notice');
        if (!$n || empty($n['msg'])) return;
        delete_transient('allspice_admin_notice');
        $type = in_array(($n['type'] ?? ''), ['success','error','warning','info'], true)
            ? (string)$n['type']
            : 'info';
        echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible">';
        echo '<p>' . esc_html((string)$n['msg']) . '</p>';
        echo '</div>';
    }, 1);
});

function allspice_admin_card_open(string $kicker, string $title = '', bool $accent = false): void {
    echo '<div class="allspice-admin__card">';
    if ($accent) echo '<div class="allspice-admin__accent"></div>';
    echo '<div class="allspice-admin__body">';
    if ($kicker !== '') echo '<p class="allspice-admin__kicker">' . esc_html($kicker) . '</p>';
    if ($title !== '') echo '<h2 class="allspice-admin__sectionTitle">' . esc_html($title) . '</h2>';
}

function allspice_admin_card_close(): void {
    echo '</div></div>';
}

function allspice_render_marketing_section(array $s): void {
    $portal_base = 'https://www.allspicelabs.com';
    $login_url = $portal_base . '/login/';
    $register_url = $portal_base . '/signup/';
    $logo_url = 'https://firebasestorage.googleapis.com/v0/b/allspice-f3bfd.firebasestorage.app/o/hosted_assets%2Fallspice_combined_red_full_registered.png?alt=media';
    echo '<div class="allspice-mkt"><div class="allspice-mkt__card"><div class="allspice-mkt__accent"></div>';
    echo '<div class="allspice-mkt__head">';
    echo '<div class="allspice-mkt__brand"><img src="' . esc_url($logo_url) . '" alt="Allspice" loading="lazy"/><div class="allspice-mkt__tag">Tools that work with your existing site</div></div>';
    echo '<div class="allspice-mkt__ctaTop">';
    echo '<a class="allspice-mkt__btnPrimary" href="' . esc_url($login_url) . '" target="_blank" rel="noopener noreferrer">Manage in Publisher Portal</a>';
    echo '<a class="allspice-mkt__btnSecondary" href="' . esc_url($register_url) . '" target="_blank" rel="noopener noreferrer">Create account</a>';
    echo '</div></div>';
    echo '<div class="allspice-mkt__inner">';
    echo '<div><p class="allspice-mkt__leftTitle">HOW IT WORKS</p><div class="allspice-mkt__grid">';
    echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">Save &amp; Organize Recipes</p><p class="allspice-mkt__featureBody">Save recipes across any site in our ecosystem. The only platform that lets readers build a collection from multiple recipe sites.</p></div>';
    echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">Smart Grocery Lists</p><p class="allspice-mkt__featureBody">AI-powered grocery lists that understand ingredients, suggest substitutions, and integrate with shopping services.</p></div>';
    echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">Create Meal Plans</p><p class="allspice-mkt__featureBody">Plan entire weeks of meals from your recipes. Readers organize dinners, get suggestions, and send lists to Amazon Fresh.</p></div>';
    echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">Track Pantry</p><p class="allspice-mkt__featureBody">Check what readers already have, get substitution suggestions, and alerts for items running low or expiring soon.</p></div>';
    echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">Cook Mode</p><p class="allspice-mkt__featureBody">Step-by-step view for phone-in-hand cooking. No more scrolling between ingredients and instructions.</p></div>';
    echo '<div class="allspice-mkt__feature"><p class="allspice-mkt__featureTitle">AI Chat Agent</p><p class="allspice-mkt__featureBody">Intelligent cooking assistant that helps customize recipes, troubleshoot issues, and answer questions in real-time.</p></div>';
    echo '</div></div>';
    echo '<div class="allspice-mkt__right"><p class="allspice-mkt__stepsTitle">SETUP</p><ol class="allspice-mkt__steps">';
    echo '<li>Log in to the Publisher Portal</li>';
    echo '<li>Add your site to get a <strong>Domain ID</strong></li>';
    echo '<li>Copy your <strong>Publisher ID</strong> + <strong>Webhook Token</strong> into this page</li>';
    echo '<li>Click <strong>Sync All</strong></li>';
    echo '</ol><div class="allspice-mkt__note">Don&#039;t have an account yet? Create one at <a href="' . esc_url($register_url) . '" target="_blank" rel="noopener noreferrer">https://www.allspicelabs.com/signup/</a> to get your Publisher ID.</div></div>';
    echo '</div></div></div>';
}

function allspice_render_premium_banner(array $s): void {
    $premium = (bool)($s['premium'] ?? true);
    if ($premium) return;
    $portal_url = 'https://portal.allspicelabs.com/login/';
    echo '<div class="allspice-premium-banner">';
    echo '<div>';
    echo '<p class="allspice-premium-banner__title">Premium subscription required</p>';
    echo '<p class="allspice-premium-banner__body">This site is connected to Allspice, but features are not active for this domain until billing setup is completed in the Publisher Portal.</p>';
    echo '</div>';
    echo '<a class="allspice-premium-banner__button" href="' . esc_url($portal_url) . '" target="_blank" rel="noopener noreferrer">Open Publisher Portal</a>';
    echo '</div>';
}

add_action('admin_menu', function () {
    $hook = add_options_page(
        'Allspice',
        'Allspice',
        'manage_options',
        ALLSPICE_SETTINGS_PAGE,
        'allspice_render_settings_page'
    );
    add_action('load-' . $hook, function () {
        nocache_headers();
    });
});

add_action('admin_init', function () {
    register_setting(
        ALLSPICE_SETTINGS_GROUP,
        ALLSPICE_SETTINGS_OPTION,
        ['sanitize_callback' => 'allspice_sanitize_settings']
    );
    add_settings_section(
        'allspice_main',
        '',
        function () {
            echo '<p class="description" style="max-width:700px;">Configure your Allspice integration. These settings control how buttons are linked and refreshed from Allspice.</p>';
        },
        ALLSPICE_SETTINGS_PAGE
    );
    add_settings_field('partner_id', 'Publisher ID', 'allspice_field_partner_id', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
    add_settings_field('domain_id', 'Domain ID', 'allspice_field_domain_id', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
    add_settings_field('webhook_token', 'Webhook Token', 'allspice_field_webhook_token', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
    add_settings_field('widget_variant', 'Widget Experience', 'allspice_field_widget_variant', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
    add_settings_field('float_widget_style', 'Widget Style', 'allspice_field_float_widget_style', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
    add_settings_field('float_launcher_options', 'Adaptive Rail', 'allspice_field_float_launcher_options', ALLSPICE_SETTINGS_PAGE, 'allspice_main');
});

function allspice_sanitize_settings($input): array {
    $cur = allspice_opt_get();
    $in = is_array($input) ? $input : [];
    $out = array_merge($cur, $in);
    if (isset($in['partner_id'])) {
        $out['partner_id'] = sanitize_text_field((string)$in['partner_id']);
    }
    if (isset($in['domain_id'])) {
        $out['domain_id'] = sanitize_text_field((string)$in['domain_id']);
    }
    if (isset($in['webhook_token'])) {
        $out['webhook_token'] = sanitize_text_field((string)$in['webhook_token']);
    }
    if (isset($in['widget_variant'])) {
        $variant = sanitize_key((string)$in['widget_variant']);
        $out['widget_variant'] = in_array($variant, ['standard', 'float'], true) ? $variant : 'standard';
    }

    /*
     * Float launcher appearance. These only affect the float variant — the standard variant's
     * floating button and WIDCON-d29x rail never read them.
     */
    $allspice_enum = static function ($value, array $allowed, string $fallback): string {
        $value = sanitize_key((string)$value);
        return in_array($value, $allowed, true) ? $value : $fallback;
    };
    $allspice_float_enums = [
        'float_desktop_style' => [['floating', 'inline'], 'floating'],
        'float_mobile_style' => [['floating', 'inline'], 'floating'],
        'float_inline_border_width' => [['0', '1', '2'], '1'],
        'float_inline_radius' => [['0', '8', '12', '16', '24'], '16'],
        'float_inline_pad_y' => [['8', '14', '22'], '14'],
        'float_launcher_side' => [['left', 'right'], 'right'],
        'float_rail_anchor' => [['top', 'middle', 'bottom'], 'bottom'],
        'float_rail_size' => [['default', 'small'], 'default'],
        'float_rail_mode' => [['dark', 'light', 'auto'], 'light'],
        'float_rail_accent_mode' => [['zone', 'single'], 'zone'],
        'float_rail_announce' => [['zone', 'jump'], 'zone'],
    ];
    foreach ($allspice_float_enums as $key => $spec) {
        if (isset($in[$key])) {
            $out[$key] = $allspice_enum($in[$key], $spec[0], $spec[1]);
        }
    }
    foreach ([
        'float_rail_accent' => '#3d7dd6',
        'float_inline_bg' => '#ffffff',
        'float_inline_border_color' => '#e7e8ee',
    ] as $key => $fallback) {
        if (isset($in[$key])) {
            $hex = sanitize_hex_color(trim((string)$in[$key]));
            $out[$key] = $hex ? $hex : $fallback;
        }
    }
    if (isset($in['float_rail_announce_ms'])) {
        $out['float_rail_announce_ms'] = max(0, min(15000, (int)$in['float_rail_announce_ms']));
    }
    /*
     * Unchecked checkboxes post nothing. The renderer emits a hidden companion input for each, so
     * the key is always present when the section rendered; absence therefore means the section was
     * not on the submitted form and the stored value must be preserved rather than silently cleared.
     */
    foreach (['float_rail_announce_search', 'float_rail_pulse', 'float_rail_hover_expand', 'float_rail_show_jump'] as $key) {
        if (array_key_exists($key, $in)) {
            $out[$key] = !empty($in[$key]);
        }
    }
    return $out;
}

function allspice_field_partner_id(): void {
    $s = allspice_opt_get();
    echo '<input type="text" class="regular-text" name="' . esc_attr(ALLSPICE_SETTINGS_OPTION) . '[partner_id]" value="' . esc_attr((string)$s['partner_id']) . '"/>';
    echo '<p class="description">Get your <strong>Publisher ID</strong> from the <a href="' . esc_url('https://www.allspicelabs.com/login/') . '" target="_blank" rel="noopener noreferrer">Allspice Publisher Portal</a>.</p>';
}

function allspice_field_domain_id(): void {
    $s = allspice_opt_get();
    echo '<input type="text" class="regular-text" name="' . esc_attr(ALLSPICE_SETTINGS_OPTION) . '[domain_id]" value="' . esc_attr((string)$s['domain_id']) . '"/>';
    echo '<p class="description">Identifies this WordPress site (from the Publisher Portal).</p>';
}

function allspice_field_webhook_token(): void {
    $s = allspice_opt_get();
    echo '<input type="password" class="regular-text" autocomplete="new-password" name="' . esc_attr(ALLSPICE_SETTINGS_OPTION) . '[webhook_token]" value="' . esc_attr((string)$s['webhook_token']) . '"/>';
    echo '<p class="description">Shared secret used for secure communication with Allspice.</p>';
}

function allspice_field_widget_variant(): void {
    $settings = allspice_opt_get();
    $current = allspice_get_widget_variant();
    echo '<select name="' . esc_attr(ALLSPICE_SETTINGS_OPTION) . '[widget_variant]">';
    echo '<option value="standard" ' .
        selected($current, 'standard', false) .
        '>Standard widget</option>';
    echo '<option value="float" ' .
        selected($current, 'float', false) .
        '>Floating card widget</option>';
    echo '</select>';
    echo '<p class="description">';
    echo '<strong>Standard:</strong> uses the existing floating button and rail. ';
    echo '<strong>Floating card:</strong> uses the adaptive rail launcher.';
    echo '</p>';
}

/**
 * Collapsible appearance options for the adaptive rail.
 *
 * The whole row is hidden unless Widget Experience is set to "Floating card", because none of it
 * applies to the standard variant. Hiding is done in JS against the live value of the variant
 * select so it responds before the page is saved; the row also starts hidden server-side so it
 * never flashes for standard sites.
 */
/*
 * Widget Style: per-device Floating/Inline choice + the inline block's chrome. Lives at the top
 * level (not inside the Adaptive Rail section) because it decides WHAT renders, not how the rail
 * looks. Shown only for the float variant, toggled live with the Widget Experience select.
 */
function allspice_field_float_widget_style(): void {
    $o = allspice_get_float_launcher_options();
    $name = esc_attr(ALLSPICE_SETTINGS_OPTION);
    $is_float = allspice_is_float_variant();

    $select = static function (string $key, array $choices, string $current) use ($name): void {
        echo '<select name="' . $name . '[' . esc_attr($key) . ']">';
        foreach ($choices as $value => $label) {
            echo '<option value="' . esc_attr((string)$value) . '" ' .
                selected($current, (string)$value, false) . '>' . esc_html($label) . '</option>';
        }
        echo '</select>';
    };
    $color = static function (string $key, string $current) use ($name): void {
        echo '<input type="text" class="allspice-color-field regular-text" style="max-width:120px" ' .
            'name="' . $name . '[' . esc_attr($key) . ']" value="' . esc_attr($current) . '" ' .
            'placeholder="default" />';
    };

    echo '<div id="allspice-float-style-options"' . ($is_float ? '' : ' style="display:none;"') . '>';

    echo '<p style="margin:0 0 4px;">Desktop: ';
    $select('float_desktop_style', ['floating' => 'Floating (bottom corner overlay)', 'inline' => 'Inline (above the recipe card)'], $o['float_desktop_style']);
    echo ' &nbsp; Mobile: ';
    $select('float_mobile_style', ['floating' => 'Floating (bottom corner overlay)', 'inline' => 'Inline (above the recipe card)'], $o['float_mobile_style']);
    echo '</p>';
    echo '<p class="description" style="max-width:640px;">Inline renders the same widget in the page flow above ' .
        'the recipe card, so it can never cover floating video or adhesion ads. The rail launcher works with ' .
        'both: with Inline it scrolls to the widget instead of opening the overlay.</p>';

    echo '<div style="margin-top:10px;"><strong>Inline block</strong></div>';
    echo '<p style="margin:4px 0 0;">Background: ';
    $color('float_inline_bg', $o['float_inline_bg']);
    echo ' &nbsp; Border: ';
    $color('float_inline_border_color', $o['float_inline_border_color']);
    echo ' ';
    $select('float_inline_border_width', ['0' => 'No border', '1' => '1 px', '2' => '2 px'], $o['float_inline_border_width']);
    echo '</p><p style="margin:6px 0 0;">Corners: ';
    $select('float_inline_radius', ['0' => 'Square', '8' => '8 px', '12' => '12 px', '16' => '16 px', '24' => '24 px'], $o['float_inline_radius']);
    echo ' &nbsp; Vertical spacing: ';
    $select('float_inline_pad_y', ['8' => 'Compact', '14' => 'Default', '22' => 'Roomy'], $o['float_inline_pad_y']);
    echo '</p>';
    echo '<p class="description" style="max-width:640px;">Only used when a device is set to the ' .
        '<strong>Inline</strong> style — tune these to blend the block into the surrounding page sections.</p>';

    echo '</div>';

    // Toggle with the variant select, without waiting for a save.
    echo '<script>(function(){';
    echo 'var sel=document.querySelector(\'select[name="' . $name . '[widget_variant]"]\');';
    echo 'var box=document.getElementById("allspice-float-style-options");';
    echo 'if(!sel||!box)return;';
    echo 'function s(){box.style.display=(sel.value==="float")?"":"none";}';
    echo 'sel.addEventListener("change",s);s();';
    echo '})();</script>';
}

function allspice_field_float_launcher_options(): void {
    $o = allspice_get_float_launcher_options();
    $name = esc_attr(ALLSPICE_SETTINGS_OPTION);
    $is_float = allspice_is_float_variant();

    $select = static function (string $key, array $choices, string $current) use ($name): void {
        echo '<select name="' . $name . '[' . esc_attr($key) . ']">';
        foreach ($choices as $value => $label) {
            echo '<option value="' . esc_attr((string)$value) . '" ' .
                selected($current, (string)$value, false) . '>' . esc_html($label) . '</option>';
        }
        echo '</select>';
    };
    $color = static function (string $key, string $current) use ($name): void {
        echo '<input type="text" class="allspice-color-field regular-text" style="max-width:120px" ' .
            'name="' . $name . '[' . esc_attr($key) . ']" value="' . esc_attr($current) . '" ' .
            'placeholder="default" />';
    };
    $check = static function (string $key, bool $current, string $label) use ($name): void {
        // Hidden companion so an unchecked box still posts a value (see the sanitizer).
        echo '<input type="hidden" name="' . $name . '[' . esc_attr($key) . ']" value="0" />';
        echo '<label style="display:block;margin:3px 0;"><input type="checkbox" value="1" ' .
            'name="' . $name . '[' . esc_attr($key) . ']" ' . checked($current, true, false) . '/> ' .
            esc_html($label) . '</label>';
    };
    $row = static function (string $label, callable $body): void {
        echo '<tr><th scope="row" style="padding-left:0;width:170px;font-weight:600;">' .
            esc_html($label) . '</th><td>';
        $body();
        echo '</td></tr>';
    };

    echo '<div id="allspice-float-launcher-options"' . ($is_float ? '' : ' style="display:none;"') . '>';
    echo '<p class="description" style="margin:0 0 10px;max-width:640px;">The <strong>adaptive rail</strong> ' .
        'docks to the screen edge and re-labels itself as the reader moves through the recipe. ' .
        'Tapping it opens the floating card. Only applies to the <strong>Floating card</strong> experience.</p>';

    echo '<details style="border:1px solid #dcdcde;border-radius:6px;padding:10px 12px;background:#fff;max-width:640px;">';
    echo '<summary style="cursor:pointer;font-weight:600;">Rail appearance</summary>';
    echo '<table class="form-table" role="presentation" style="margin-top:6px;"><tbody>';

    $row('Position', static function () use ($select, $o) {
        $select('float_launcher_side', ['right' => 'Right edge', 'left' => 'Left edge'], $o['float_launcher_side']);
        echo ' ';
        $select('float_rail_anchor', ['bottom' => 'Bottom', 'middle' => 'Middle', 'top' => 'Top'], $o['float_rail_anchor']);
    });
    $row('Size', static function () use ($select, $o) {
        $select('float_rail_size', ['default' => 'Default', 'small' => 'Small'], $o['float_rail_size']);
    });
    $row('Shell', static function () use ($select, $o) {
        $select('float_rail_mode', ['light' => 'Light', 'dark' => 'Dark', 'auto' => 'Auto (match visitor theme)'], $o['float_rail_mode']);
        echo '<p class="description">Rail background: white for <strong>Light</strong>, near-black for ' .
            '<strong>Dark</strong>. <strong>Auto</strong> follows each visitor&rsquo;s own light/dark setting.</p>';
    });
    $row('Accent', static function () use ($select, $color, $o) {
        $select('float_rail_accent_mode', ['zone' => 'Per section (recommended)', 'single' => 'Single color'], $o['float_rail_accent_mode']);
        echo ' ';
        $color('float_rail_accent', $o['float_rail_accent']);
        echo '<p class="description"><strong>Per section</strong> tints the icon by what the reader is ' .
            'looking at — ingredients, instructions, chat. <strong>Single color</strong> uses the color above everywhere.</p>';
    });
    $row('Announce', static function () use ($select, $o, $name) {
        $select('float_rail_announce', ['zone' => 'Once per section', 'jump' => 'Only "Jump to Recipe"'], $o['float_rail_announce']);
        echo ' <input type="number" min="0" max="15000" step="100" style="width:90px" name="' . $name .
            '[float_rail_announce_ms]" value="' . esc_attr((string)$o['float_rail_announce_ms']) . '" /> ms';
        echo '<p class="description">On recipe pages: how long the rail opens to its label before ' .
            'settling back to the edge tab. Set to 0 to leave it open.</p>';
    });
    $row('Off-recipe', static function () use ($check, $o) {
        $check('float_rail_announce_search', $o['float_rail_announce_search'], 'Announce the search rail on non-recipe pages');
        echo '<p class="description">Pages without a recipe show a single <strong>Find a Recipe</strong> ' .
            'state. This controls its announcement independently, so recipe pages and the rest of the ' .
            'site can behave differently.</p>';
    });
    $row('Behavior', static function () use ($check, $select, $o) {
        $check('float_rail_pulse', $o['float_rail_pulse'], 'Pulse the icon to draw attention');
        $check('float_rail_hover_expand', $o['float_rail_hover_expand'], 'Expand on hover (desktop only)');
        $check('float_rail_show_jump', $o['float_rail_show_jump'], 'Show "Jump to Recipe" at the top of the page');
    });

    echo '</tbody></table>';
    echo '</details></div>';

    // Toggle with the variant select, without waiting for a save.
    echo '<script>(function(){';
    echo 'var sel=document.querySelector(\'select[name="' . $name . '[widget_variant]"]\');';
    echo 'var box=document.getElementById("allspice-float-launcher-options");';
    echo 'if(!sel||!box)return;';
    echo 'function s(){box.style.display=(sel.value==="float")?"":"none";}';
    echo 'sel.addEventListener("change",s);s();';
    echo '})();</script>';
}

function allspice_admin_debug_url(string $path = '/'): string {
    $nonce = wp_create_nonce(ALLSPICE_DEBUG_NONCE_ACTION);
    $base = home_url($path);
    return add_query_arg(
        [
            ALLSPICE_DEBUG_PARAM => '1',
            ALLSPICE_DEBUG_NONCE_PARAM => $nonce,
        ],
        $base
    );
}

function allspice_render_settings_page(): void {
    if (!current_user_can('manage_options')) return;
    $s = allspice_opt_get();
    echo '<div class="wrap allspice-admin">';
    echo '<div class="allspice-admin__stack">';
    allspice_render_marketing_section($s);
    allspice_admin_card_open('CONFIGURATION', 'Connect WordPress to Allspice', true);
    echo '<form method="post" action="options.php">';
    settings_fields(ALLSPICE_SETTINGS_GROUP);
    do_settings_sections(ALLSPICE_SETTINGS_PAGE);
    submit_button('Save Changes');
    echo '</form>';
    allspice_render_premium_banner($s);
    echo '<div class="allspice-admin__divider"></div>';
    echo '<p class="allspice-admin__sectionTitle" style="margin-bottom:6px;">Synchronization</p>';
    echo '<p class="description" style="max-width:700px;margin-top:0;">Manually sync buttons and recipes from Allspice.</p>';
    
    echo '<div class="allspice-sync-actions">';
    echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
    wp_nonce_field(ALLSPICE_SYNC_BUTTONS_SETTINGS_ACTION);
    echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_SYNC_BUTTONS_SETTINGS_ACTION) . '"/>';
    submit_button('Sync Buttons & Settings', 'secondary', 'submit', false);
    echo '</form>';
    echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
    wp_nonce_field(ALLSPICE_SYNC_RECIPES_ACTION);
    echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_SYNC_RECIPES_ACTION) . '"/>';
    submit_button('Sync Recipes', 'secondary', 'submit', false);
    echo '</form>';
    echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
    wp_nonce_field(ALLSPICE_SYNC_ALL_ACTION);
    echo '<input type="hidden" name="action" value="' . esc_attr(ALLSPICE_SYNC_ALL_ACTION) . '"/>';
    submit_button('Sync All', 'primary', 'submit', false);
    echo '</form>';
    echo '</div>';
    
    allspice_admin_card_close();
    allspice_admin_card_open('ADVANCED', 'Status & Debug', false);
    $fmt = get_option('date_format') . ' ' . get_option('time_format');
    echo '<table class="widefat striped" style="margin-top:6px;">';
    echo '<tr><th>Linked Buttons</th><td>' . esc_html((string)($s['buttons_count'] ?? 0)) . '</td></tr>';
    echo '<tr><th>Last Manual Refresh</th><td>' . esc_html(!empty($s['manual_refreshed_at']) ? wp_date($fmt, (int)$s['manual_refreshed_at']) : 'Never') . '</td></tr>';
    echo '<tr><th>Last Cache Refresh</th><td>' . esc_html(!empty($s['cache_fetched_at']) ? wp_date($fmt, (int)$s['cache_fetched_at']) : 'Never') . '</td></tr>';
    echo '<tr><th>Cache TTL</th><td>' . esc_html(round(((int)($s['cache_ttl'] ?? 0)) / 3600, 2)) . ' hours</td></tr>';
    $recipes_count = (int)($s['recipes_count'] ?? 0);
    $recipes_synced_at = (int)($s['recipes_synced_at'] ?? 0);
    $recipes_last_error = (string)($s['recipes_last_error'] ?? '');
    echo '<tr><th>Synced Recipes</th><td>' . esc_html((string)$recipes_count) . '</td></tr>';
    echo '<tr><th>Last Recipe Sync</th><td>' . esc_html($recipes_synced_at ? wp_date($fmt, $recipes_synced_at) : 'Never') . '</td></tr>';
    $tp_synced_at = (int)($s['theme_policies_synced_at'] ?? 0);
    $tp_err = (string)($s['theme_policies_last_error'] ?? '');
    $tp = function_exists('allspice_theme_policies_cache_get') ? allspice_theme_policies_cache_get() : ['theme' => null, 'policy' => null];
    $theme_synced = ($tp_synced_at > 0) && ($tp['theme'] !== null);
    $policies_synced = ($tp_synced_at > 0) && ($tp['policy'] !== null);
    echo '<tr><th>Theme</th><td>' . ($theme_synced ? '<strong>Synced</strong>' : '<em>Not synced</em>') . '</td></tr>';
    echo '<tr><th>Policies</th><td>' . ($policies_synced ? '<strong>Synced</strong>' : '<em>Not synced</em>') . '</td></tr>';
    echo '<tr><th>Last Theme/Policies Sync</th><td>' . esc_html($tp_synced_at ? wp_date($fmt, (int)$tp_synced_at) : 'Never') . '</td></tr>';
    echo '<tr><th>Manual Error</th><td>' . (!empty($s['manual_last_error']) ? esc_html($s['manual_last_error']) : '<em>None</em>') . '</td></tr>';
    echo '<tr><th>Cache Error</th><td>' . (!empty($s['cache_last_error']) ? esc_html($s['cache_last_error']) : '<em>None</em>') . '</td></tr>';
    echo '<tr><th>Recipe Sync Error</th><td>' . (!empty($recipes_last_error) ? esc_html($recipes_last_error) : '<em>None</em>') . '</td></tr>';
    echo '<tr><th>Theme/Policies Sync Error</th><td>' . (!empty($tp_err) ? esc_html($tp_err) : '<em>None</em>') . '</td></tr>';
    $premium = (bool)($s['premium'] ?? true);
    $premium_checked_at = (int)($s['premium_checked_at'] ?? 0);
    echo '<tr><th>Premium</th><td>' . ($premium ? '<strong>Active</strong>' : '<strong style="color:#b42318;">Not active</strong>') . '</td></tr>';
    echo '<tr><th>Last Premium Check</th><td>' . esc_html($premium_checked_at ? wp_date($fmt, $premium_checked_at) : 'Never') . '</td></tr>';
    echo '</table>';
    echo '<div class="allspice-admin__divider"></div>';
    echo '<p class="allspice-admin__sectionTitle" style="margin-bottom:6px;">Debug</p>';
    echo '<p class="description" style="max-width:700px;margin-top:0;">Debug logging is restricted to administrators and requires a nonce. Use this link to enable debug mode on the front end.</p>';
    $debug_url = allspice_admin_debug_url('/');
    echo '<p style="margin:10px 0 8px 0;"><input type="text" class="large-text code" readonly value="' . esc_attr($debug_url) . '" /></p>';
    echo '<p style="margin:0;"><a class="button button-secondary" href="' . esc_url($debug_url) . '" target="_blank" rel="noopener noreferrer">Open Debug Mode</a></p>';
    allspice_admin_card_close();
    echo '</div>';
    echo '</div>';
}
    
function allspice_admin_post_sync_buttons_settings(): void {
    if (!current_user_can('manage_options')) wp_die('Forbidden', 403);
    check_admin_referer(ALLSPICE_SYNC_BUTTONS_SETTINGS_ACTION);
    $now = time();
    $rb = allspice_refresh_buttons_cache(true);
    if (empty($rb['ok'])) {
        allspice_opt_update(['manual_last_error' => (string)($rb['error'] ?? 'Unknown error')]);
        set_transient('allspice_admin_notice', [
            'msg' => 'Sync failed (buttons): ' . (string)($rb['error'] ?? 'Unknown error'),
            'type' => 'error'
        ], 30);
        wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
        exit;
    }
    $rtp = allspice_refresh_theme_policies_cache(true);
    if (empty($rtp['ok'])) {
        allspice_opt_update([
            'manual_refreshed_at' => $now,
            'manual_last_error' => '',
            'theme_policies_last_error' => (string)($rtp['error'] ?? 'Unknown error'),
            'theme_policies_synced_at' => $now,
        ]);
        set_transient('allspice_admin_notice', [
            'msg' => 'Buttons synced, but settings sync failed: ' . (string)($rtp['error'] ?? 'Unknown error'),
            'type' => 'error'
        ], 30);
        wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
        exit;
    }
    allspice_opt_update(['manual_refreshed_at' => $now, 'manual_last_error' => '']);
    $buttons_count = (int)($rb['count'] ?? (allspice_opt_get()['buttons_count'] ?? 0));
    $theme_ok = !empty($rtp['theme_ok']);
    $pol_ok = !empty($rtp['policy_ok']);
    set_transient('allspice_admin_notice', [
        'msg' => 'Buttons & settings synced successfully. Buttons: ' . $buttons_count .
                ' | Theme: ' . ($theme_ok ? 'synced' : 'missing') .
                ' | Policies: ' . ($pol_ok ? 'synced' : 'missing'),
        'type' => 'success'
    ], 30);
    wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
    exit;
}
    
function allspice_admin_post_sync_recipes(): void {
    if (!current_user_can('manage_options')) wp_die('Forbidden', 403);
    check_admin_referer(ALLSPICE_SYNC_RECIPES_ACTION);
    $now = time();
    $rr = allspice_refresh_recipes_cache(true);
    if (empty($rr['ok'])) {
        allspice_opt_update([
            'recipes_last_error' => (string)($rr['error'] ?? 'Unknown error'),
            'recipes_synced_at'  => $now,
        ]);
        set_transient('allspice_admin_notice', [
            'msg' => 'Recipe sync failed: ' . (string)($rr['error'] ?? 'Unknown error'),
            'type' => 'error'
        ], 30);
        wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
        exit;
    }
    $recipe_count = (int)($rr['count'] ?? 0);
    $premium = (bool)($rr['premium'] ?? true);
    set_transient('allspice_admin_notice', [
        'msg' => 'Recipes synced successfully. Recipes: ' . $recipe_count .
                ($premium ? '' : ' | Premium is not active for this domain.'),
        'type' => $premium ? 'success' : 'warning'
    ], 30);
    wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
    exit;
}
    
function allspice_admin_post_sync_all(): void {
    if (!current_user_can('manage_options')) wp_die('Forbidden', 403);
    check_admin_referer(ALLSPICE_SYNC_ALL_ACTION);
    $now = time();
    $rb = allspice_refresh_buttons_cache(true);
    if (empty($rb['ok'])) {
        allspice_opt_update(['manual_last_error' => (string)($rb['error'] ?? 'Unknown error')]);
        set_transient('allspice_admin_notice', [
            'msg' => 'Sync failed (buttons): ' . (string)($rb['error'] ?? 'Unknown error'),
            'type' => 'error'
        ], 30);
        wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
        exit;
    }
    allspice_opt_update(['manual_refreshed_at' => $now, 'manual_last_error' => '']);
    $rr = allspice_refresh_recipes_cache(true);
    if (empty($rr['ok'])) {
        allspice_opt_update([
            'recipes_last_error' => (string)($rr['error'] ?? 'Unknown error'),
            'recipes_synced_at'  => $now,
        ]);
        set_transient('allspice_admin_notice', [
            'msg' => 'Buttons synced, but recipe sync failed: ' . (string)($rr['error'] ?? 'Unknown error'),
                'type' => 'error'
        ], 30);
        wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
        exit;
    }
    $rtp = allspice_refresh_theme_policies_cache(true);
    if (empty($rtp['ok'])) {
        allspice_opt_update([
            'theme_policies_last_error' => (string)($rtp['error'] ?? 'Unknown error'),
            'theme_policies_synced_at'  => $now,
        ]);
        set_transient('allspice_admin_notice', [
            'msg' => 'Buttons + recipes synced, but settings sync failed: ' . (string)($rtp['error'] ?? 'Unknown error'),
            'type' => 'error'
        ], 30);
        wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
        exit;
    }
    $buttons_count = (int)($rb['count'] ?? (allspice_opt_get()['buttons_count'] ?? 0));
    $recipe_count = (int)($rr['count'] ?? 0);
    $premium = (bool)($rr['premium'] ?? true);
    $theme_ok = !empty($rtp['theme_ok']);
    $pol_ok = !empty($rtp['policy_ok']);
    set_transient('allspice_admin_notice', [
        'msg'  => 'Synced successfully. Buttons: ' . $buttons_count .
                ' | Recipes: ' . $recipe_count .
                ' | Theme: ' . ($theme_ok ? 'synced' : 'missing') .
                ' | Policies: ' . ($pol_ok ? 'synced' : 'missing') .
                ($premium ? '' : ' | Premium is not active for this domain.'),
        'type' => $premium ? 'success' : 'warning'
    ], 30);
    wp_safe_redirect(admin_url('options-general.php?page=' . ALLSPICE_SETTINGS_PAGE));
    exit;
}
    
add_action('admin_post_' . ALLSPICE_SYNC_BUTTONS_SETTINGS_ACTION, 'allspice_admin_post_sync_buttons_settings');
add_action('admin_post_' . ALLSPICE_SYNC_RECIPES_ACTION, 'allspice_admin_post_sync_recipes');
add_action('admin_post_' . ALLSPICE_SYNC_ALL_ACTION, 'allspice_admin_post_sync_all');