<?php
declare(strict_types=1);

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

/**
 * Pricing Plans element renderer (shared).
 *
 * Phase 1, step 24. Pricing cards with monthly/yearly toggle, ribbon,
 * hover effects (lift / tilt / glow / float), and an optional "arc"
 * fan that rotates the cards in 3D space with cursor + gyro parallax.
 *
 * Both renderers were already strongly aligned. Real divergence:
 *
 *   - Frontend used `pricing-<el_id>` as the wrapper id; editor used
 *     `pricing-<uniqid()>` (different per render call). Unified to
 *     the stable id-based form so CSS rules and re-renders work
 *     consistently across both contexts.
 *
 *   - Hover CSS selector pinned to `#<id>` (frontend) vs
 *     `.impulse-el[data-id="N"]` (editor). Unified to `#<id>` in
 *     both — the stable id works regardless of wrapper context.
 *
 *   - Frontend had richer mobile behaviour in the arc-CSS @media
 *     block (vertical column with X-axis fan instead of Y, font
 *     compression for tiny cards). Editor's mobile path was simpler.
 *     Unified forward (frontend canonical).
 *
 *   - Frontend's arc JS includes gyroscope-driven parallax on touch
 *     devices (DeviceOrientation) + the runtime axis switch (Y on
 *     desktop / X on mobile). Editor's was simpler. Unified forward.
 *
 *   - Frontend emits responsive @media for `price_size` /
 *     `btn_font_size` / `border_radius` via impulse_responsive_css.
 *     Editor doesn't (preview re-renders per device tab). Preserved.
 *
 *   - Editor supports button_type='subscription' (URL pattern
 *     `?subscribe=<id>`). Frontend pre-extraction didn't. Adopting
 *     the editor's path so the panel setting works on live too.
 *
 * @sideeffect Emits wp_add_inline_style calls for the arc CSS,
 *             responsive size @media, and hover effect rules.
 *             Emits an inline <script> for the arc parallax + toggle.
 */
function impulse_element_render_plans($el, array $settings, string $context): string {
    if (is_object($el)) $el = (array) $el;
    $elId     = preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($el['id'] ?? '0'));
    $isEditor = ($context === 'editor');

    $read = function (string $key, $default) use ($settings, $isEditor) {
        if ($isEditor && function_exists('impulse_get_responsive')) {
            return impulse_get_responsive($settings, $key, $default);
        }
        return $settings[$key] ?? $default;
    };

    // ── Plans array (default to 3-tier demo) ─────────────────────
    $plans = $settings['plans'] ?? [];
    if (is_string($plans)) $plans = json_decode($plans, true) ?: [];
    if (empty($plans)) {
        $plans = [
            ['title' => 'Basic',      'subtitle' => 'For individuals',  'price' => '19', 'currency' => '$', 'period' => '/month', 'yearly_price' => '190', 'yearly_period' => '/year', 'features' => [['text'=>'5 Projects','status'=>'included'],['text'=>'10GB Storage','status'=>'included'],['text'=>'Email Support','status'=>'included'],['text'=>'Priority Support','status'=>'excluded']], 'button_text' => 'Get Started', 'button_link' => '#', 'ribbon' => 'none'],
            ['title' => 'Pro',        'subtitle' => 'For small teams',  'price' => '49', 'currency' => '$', 'period' => '/month', 'yearly_price' => '490', 'yearly_period' => '/year', 'features' => [['text'=>'Unlimited Projects','status'=>'included'],['text'=>'100GB Storage','status'=>'included'],['text'=>'Priority Support','status'=>'included'],['text'=>'API Access','status'=>'included']], 'button_text' => 'Get Started', 'button_link' => '#', 'ribbon' => 'popular', 'highlighted' => true],
            ['title' => 'Enterprise', 'subtitle' => 'For large teams',  'price' => '99', 'currency' => '$', 'period' => '/month', 'yearly_price' => '990', 'yearly_period' => '/year', 'features' => [['text'=>'Everything in Pro','status'=>'included'],['text'=>'Unlimited Storage','status'=>'included'],['text'=>'Dedicated Support','status'=>'included'],['text'=>'Custom Integrations','status'=>'included']], 'button_text' => 'Contact Sales', 'button_link' => '#', 'ribbon' => 'none'],
        ];
    }

    // ── Block-level settings ─────────────────────────────────────
    $showToggle  = !empty($settings['show_toggle']);
    $yearlyLabel = (string) ($settings['yearly_label'] ?? 'Save 20%');
    $ribbonStyle = (string) ($settings['ribbon_style'] ?? 'banner');
    $hoverEffect = (string) ($settings['hover_effect'] ?? 'none');

    // Card colors (3 explicit per type, rest derived).
    $bgColor       = (string) ($settings['bg_color']           ?? '#ffffff');
    $textColor     = (string) ($settings['text_color']         ?? '#666666');
    $buttonBg      = (string) ($settings['button_bg']          ?? '#1d2327');
    $borderColor   = function_exists('impulse_derive_border')   ? impulse_derive_border($bgColor)        : '#e0e0e0';
    $titleColor    = function_exists('impulse_contrast_color') ? impulse_contrast_color($bgColor)        : '#1d2327';
    $toggleLabelColor = $titleColor;
    $buttonColor   = function_exists('impulse_contrast_color') ? impulse_contrast_color($buttonBg)      : '#ffffff';
    $checkColor    = $textColor;
    $crossColor    = $textColor;
    $checkIcon     = 'check-square';
    $crossIcon     = 'x';
    $ribbonBg      = $buttonBg;

    // Highlighted card.
    $highlightBg          = (string) ($settings['highlight_bg']        ?? '#1d2327');
    $highlightColor       = (string) ($settings['highlight_color']     ?? '#ffffff');
    $highlightButtonBg    = (string) ($settings['highlight_button_bg'] ?? '#ffffff');
    $highlightBorder      = $highlightBg;
    $highlightButtonColor = function_exists('impulse_contrast_color') ? impulse_contrast_color($highlightButtonBg) : '#1d2327';
    $highlightCheckColor  = $highlightColor;
    $highlightCrossColor  = $highlightColor;
    $highlightRibbonBg    = $highlightButtonBg;

    $priceColor           = $titleColor;
    $ribbonColor          = function_exists('impulse_contrast_color') ? impulse_contrast_color($ribbonBg) : '#ffffff';
    $highlightRibbonColor = function_exists('impulse_contrast_color') ? impulse_contrast_color($highlightRibbonBg) : '#1d2327';

    $borderRadius   = intval($read('border_radius', 4));
    $priceSize      = intval($settings['price_size']    ?? 38);
    $btnFontSize    = intval($settings['btn_font_size'] ?? 14);
    $btnPaddingV    = intval($settings['btn_padding_v'] ?? 14);
    $sectionMaxWidth = intval($settings['section_max_width'] ?? 1200);

    $visClass = (!$isEditor && function_exists('impulse_get_visibility_classes'))
        ? impulse_get_visibility_classes($settings) : '';

    // Stable id (pre-extraction editor used uniqid() — replaced for consistency).
    $uniqueId = 'pricing-' . ($elId ?: (function_exists('wp_rand') ? wp_rand(1000, 9999) : random_int(1000, 9999)));

    // ── Hover effect: routed via `data-hover` on the wrapper ─────
    // Pre-consolidation (pre-2.1.258) this case emitted ~10 lines of inline
    // CSS per plans block, all keyed to #pricing-N selectors. Every rule
    // body was identical for every plans block on every page; only the id
    // prefix differed. Moved to assets/css/impulse-plans.css with
    // `.impulse-pricing-wrapper[data-hover="..."]` selectors. The glow
    // effect's per-instance highlight colour is now surfaced as a CSS
    // variable on the wrapper (--impulse-plans-glow), consumed by the
    // static stylesheet via color-mix().
    //
    // The static stylesheet is singleton-enqueued at the bottom of this
    // function. Per-instance responsive overrides (border_radius, price
    // size, button font size) still emit inline since those embed the
    // designer's saved per-device values.

    // ── Wrapper ──────────────────────────────────────────────────
    $wrapperStyle = 'width:100%;margin-left:auto;margin-right:auto;';
    if ($sectionMaxWidth > 0) $wrapperStyle .= 'max-width:' . $sectionMaxWidth . 'px;';
    $animAttrs = (!$isEditor && function_exists('impulse_get_animation_attrs'))
        ? impulse_get_animation_attrs($settings)
        : ['class' => '', 'style' => ''];
    $wrapClass = 'impulse-pricing-wrapper';
    if ($visClass !== '')              $wrapClass .= ' ' . $visClass;
    if (!empty($animAttrs['class']))   $wrapClass .= ' ' . $animAttrs['class'];
    if (!empty($animAttrs['style']))   $wrapperStyle .= $animAttrs['style'];

    // ── Arc settings ─────────────────────────────────────────────
    $planCount     = count($plans);
    $planCols      = max(1, min($planCount, 4));
    $arcIntensity  = max(0, min(100, intval($settings['arc_intensity'] ?? 0)));
    $arcActive     = $arcIntensity > 0 && $planCount > 1;
    // Arc defaults to OFF on mobile (the rotated cards crash into the
    // sides of small viewports). Designers re-enable it by raising the
    // Arc slider on the MOBILE device tab (arc_intensity_mobile > 0) -
    // the dedicated "Arc on mobile" checkbox was removed 2026-06-10.
    // The flag still surfaces as data-mobile-arc on the wrapper; CSS in
    // impulse-plans.css strips arc transforms + perspective when it's
    // absent, and the inline JS apply() loop checks the same flag.
    $arcMobile     = intval($settings['arc_intensity_mobile'] ?? 0) > 0;
    $arcMaxAngle   = 25.0;
    $arcAngleScale = $arcIntensity / 100.0;
    $arcCenterIdx  = ($planCount - 1) / 2.0;

    $gridClass = 'impulse-pricing-grid' . ($arcActive ? ' impulse-pricing-arc' : '');
    // The editor's Mobile tab narrows the CANVAS, not the viewport, so the
    // frontend's @media(max-width:767px) rule never fires here. Stack in the
    // style itself, with the same 1 column / 16px gap the frontend uses.
    $planDevice = $isEditor ? ($GLOBALS['impulse_preview_device'] ?? 'desktop') : 'desktop';
    $gridStyle = ($isEditor && $planDevice === 'mobile')
        ? 'display:grid;grid-template-columns:1fr;gap:16px;align-items:stretch;'
        : 'display:grid;grid-template-columns:repeat(' . $planCols . ',1fr);gap:24px;align-items:stretch;';
    if ($arcActive) {
        $gridStyle .= 'perspective:1200px;perspective-origin:center top;transform-style:preserve-3d;';
    }

    // Arc 3D base + mobile fan/compression CSS — same body for every
    // plans block on every page. Now lives in impulse-plans.css alongside
    // the hover effects (singleton-enqueued via `<link>` below). Removed
    // ~22 lines of inline `wp_add_inline_style` emission per render.

    // ── Compose HTML ─────────────────────────────────────────────
    ob_start();
    // Surface the glow colour as a CSS variable for the consolidated
    // hover stylesheet (impulse-plans.css). Other effects don't need
    // per-instance values, only the data-hover attribute below.
    $hoverStyleExtra = '';
    if ($hoverEffect === 'glow') {
        $hoverStyleExtra = ';--impulse-plans-glow:' . esc_attr($buttonBg);
    }
    echo '<div class="' . esc_attr($wrapClass) . '" id="' . esc_attr($uniqueId) . '" data-hover="' . esc_attr($hoverEffect) . '"';
    if ($arcMobile) echo ' data-mobile-arc="' . esc_attr('1') . '"';
    echo ' style="' . esc_attr($wrapperStyle . $hoverStyleExtra) . '">';

    // Emit the static hover/arc/mobile stylesheet INLINE with every plans
    // block. An external <link rel="stylesheet"> does NOT survive the
    // screen-mode page's render pipeline — a body-level <link> echoed from
    // element output never reaches the served HTML (confirmed via raw curl:
    // the tag is gone, while inline <style> blocks from element renderers
    // survive intact). The result was the genuine Plans block shipping with
    // zero CSS and losing its entire mobile 2-column layout, while the demo
    // sample (captured into an inert JS string before the pipeline runs)
    // still showed the rules. Inlining the rules in a <style> is the format
    // that reliably survives. Read once per request via a static cache.
    // Ungated so each block carries the rules even when an earlier sample
    // render (builder demo) already ran — duplicate identical <style> is
    // harmless; real pages carry one Plans block. 2026-05-28.
    static $_plCss = null;
    if ($_plCss === null) {
        $_plDir  = defined('INSTINCTOR_PLUGIN_DIR') ? INSTINCTOR_PLUGIN_DIR : (dirname(__DIR__, 2) . '/');
        $_plFile = rtrim($_plDir, '/\\') . '/assets/css/impulse-plans.css';
        $_plCss  = is_readable($_plFile) ? (string) file_get_contents($_plFile) : '';
    }
    if ($_plCss !== '') {
        wp_add_inline_style('instinctor-page-inline', $_plCss);
    }

    // Monthly/Yearly Toggle.
    if ($showToggle) {
        echo '<div class="impulse-pricing-toggle" style="display:flex;justify-content:center;gap:8px;margin-bottom:32px;align-items:center">';
        echo '<span class="impulse-toggle-label impulse-toggle-monthly" style="font-size:14px;font-weight:500;color:' . esc_attr($toggleLabelColor) . '">Monthly</span>';
        echo '<label style="position:relative;display:inline-block;width:50px;height:26px;cursor:pointer">';
        echo '<input type="checkbox" class="impulse-billing-toggle" style="opacity:0;width:0;height:0">';
        echo '<span style="position:absolute;inset:0;background:#ddd;border-radius:26px;transition:0.3s"></span>';
        echo '<span class="impulse-toggle-knob" style="position:absolute;left:3px;top:3px;width:20px;height:20px;background:#fff;border-radius:50%;transition:0.3s;box-shadow:0 1px 3px rgba(0,0,0,0.2)"></span>';
        echo '</label>';
        echo '<span class="impulse-toggle-label impulse-toggle-yearly" style="font-size:14px;font-weight:500;color:' . esc_attr($toggleLabelColor) . ';opacity:0.5">Yearly</span>';
        if ($yearlyLabel !== '') {
            echo '<span style="background:' . esc_attr($ribbonBg) . ';color:' . esc_attr($ribbonColor) . ';font-size:11px;padding:4px 8px;border-radius:4px;font-weight:500;display:inline-flex;align-items:center;line-height:1">' . esc_html($yearlyLabel) . '</span>';
        }
        echo '</div>';
    }

    echo '<div class="' . esc_attr($gridClass) . '" style="' . esc_attr($gridStyle) . '">';

    // ── Per-plan render ──────────────────────────────────────────
    $planIdx = -1;
    foreach ($plans as $plan) {
        $planIdx++;
        $isHighlighted     = !empty($plan['highlighted']);
        $planTitle         = (string) ($plan['title']         ?? 'Plan');
        $planSubtitle      = (string) ($plan['subtitle']      ?? '');
        $planPrice         = (string) ($plan['price']         ?? '0');
        $planYearlyPrice   = (string) ($plan['yearly_price']  ?? $planPrice);
        $planCurrency      = (string) ($plan['currency']      ?? '$');
        $planPeriod        = (string) ($plan['period']        ?? '/month');
        $planYearlyPeriod  = (string) ($plan['yearly_period'] ?? '/year');
        $planOriginalPrice = (string) ($plan['original_price'] ?? '');
        $planFeatures      = $plan['features'] ?? [];
        if (is_string($planFeatures)) $planFeatures = json_decode($planFeatures, true) ?: [];
        $planButtonText    = (string) ($plan['button_text']    ?? 'Get Started');
        $planButtonLink    = (string) ($plan['button_link']    ?? '#');
        $planButtonType    = (string) ($plan['button_type']    ?? 'custom');
        $planProductId     = $plan['product_id']      ?? '';
        $planPageId        = $plan['page_id']         ?? '';
        $planSubscriptionId = $plan['subscription_id'] ?? '';
        $planRibbon        = (string) ($plan['ribbon']      ?? 'none');
        $planRibbonText    = (string) ($plan['ribbon_text'] ?? '');

        // Button href resolution: custom URL / product / page / subscription.
        $buttonHref = $planButtonLink;
        if ($planButtonType === 'product' && $planProductId !== '') {
            $buttonHref = function_exists('home_url') ? home_url('?add-to-cart=' . $planProductId) : '/?add-to-cart=' . $planProductId;
        } elseif ($planButtonType === 'page' && $planPageId !== '' && class_exists('\\Instinctor\\Services\\PageService')) {
            $buttonHref = \Instinctor\Services\PageService::getPageUrl((int) $planPageId) ?: '#';
        } elseif ($planButtonType === 'subscription' && $planSubscriptionId !== '') {
            $buttonHref = function_exists('home_url') ? home_url('?subscribe=' . $planSubscriptionId) : '/?subscribe=' . $planSubscriptionId;
        }

        $cardBg          = $isHighlighted ? $highlightBg          : $bgColor;
        $cardTitleColor  = $isHighlighted ? $highlightColor       : $titleColor;
        $cardPriceColor  = $isHighlighted ? $highlightColor       : $priceColor;
        $cardTextColor   = $isHighlighted ? 'rgba(255,255,255,0.8)' : $textColor;
        $cardBtnBg       = $isHighlighted ? $highlightButtonBg    : $buttonBg;
        $cardBtnColor    = $isHighlighted ? $highlightButtonColor : $buttonColor;
        $cardRibbonBg    = $isHighlighted ? $highlightRibbonBg    : $ribbonBg;
        $cardRibbonColor = $isHighlighted ? $highlightRibbonColor : $ribbonColor;

        // Card style + arc base rotation.
        $cardStyle = 'background:' . esc_attr($cardBg) . ';border-radius:' . $borderRadius . 'px;position:relative;display:flex;flex-direction:column;overflow:hidden;transform-style:preserve-3d;';
        $cardArcBase = 0;
        if ($arcActive && $arcCenterIdx > 0) {
            $relPos = ($arcCenterIdx - $planIdx) / $arcCenterIdx;
            $cardArcBase = round($relPos * $arcMaxAngle * $arcAngleScale, 2);
            $cardStyle .= 'transform:rotateY(' . $cardArcBase . 'deg);';
        }

        // Z-ladder for interior layers (parallax with card rotation).
        $zBoost    = $isHighlighted ? 15 : 0;
        $zRibbon   = 50 + $zBoost;
        $zHeader   = 15 + $zBoost;
        $zPrice    = 30 + $zBoost;
        $zFeatures = 8  + $zBoost;
        $zButton   = 40 + $zBoost;

        echo '<div class="impulse-pricing-card' . ($isHighlighted ? ' highlighted' : '') . '"';
        if ($arcActive) echo ' data-arc-base="' . esc_attr((string) $cardArcBase) . '"';
        echo ' style="' . esc_attr($cardStyle) . '">';

        // Ribbon slot (fixed 36px when banner so cards align even if some have no ribbon).
        $hasRibbon = $planRibbon !== 'none' && $ribbonStyle !== 'none';
        $ribbonContainerHeight = ($ribbonStyle === 'banner') ? '36px' : '0';
        echo '<div class="impulse-pricing-ribbon-slot" style="height:' . esc_attr($ribbonContainerHeight) . ';position:relative;flex-shrink:0;overflow:hidden;border-radius:' . (int) $borderRadius . 'px ' . (int) $borderRadius . 'px 0 0;transform-style:preserve-3d">';
        if ($hasRibbon && $ribbonStyle === 'banner') {
            $ribbonText = $planRibbon === 'custom' ? $planRibbonText : ucfirst($planRibbon);
            echo '<div class="impulse-pricing-ribbon-banner" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:' . esc_attr($cardRibbonBg) . ';color:' . esc_attr($cardRibbonColor) . ';font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:1px;transform:translateZ(' . (int) $zRibbon . 'px)">' . esc_html($ribbonText) . '</div>';
        }
        echo '</div>';

        // Corner ribbon (absolute, doesn't affect layout).
        if ($hasRibbon && $ribbonStyle === 'corner') {
            $ribbonText = $planRibbon === 'custom' ? $planRibbonText : ucfirst($planRibbon);
            echo '<div class="impulse-pricing-ribbon-corner" style="position:absolute;top:0;right:0;width:80px;height:80px;overflow:hidden;z-index:2;transform:translateZ(' . (int) $zRibbon . 'px);transform-style:preserve-3d">';
            echo '<div style="position:absolute;top:16px;right:-24px;background:' . esc_attr($cardRibbonBg) . ';color:' . esc_attr($cardRibbonColor) . ';padding:4px 32px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;transform:rotate(45deg)">' . esc_html($ribbonText) . '</div>';
            echo '</div>';
        }

        // Header.
        echo '<div class="impulse-pricing-header" style="padding:18px 20px 0;text-align:center;min-height:58px;display:flex;flex-direction:column;justify-content:center;transform:translateZ(' . (int) $zHeader . 'px)">';
        echo '<div class="impulse-pricing-title" style="font-size:18px;font-weight:600;color:' . esc_attr($cardTitleColor) . ';letter-spacing:-0.01em">' . esc_html($planTitle) . '</div>';
        echo '<div class="impulse-pricing-subtitle" style="font-size:14px;color:' . esc_attr($cardTextColor) . ';margin-top:4px;min-height:20px">' . ($planSubtitle !== '' ? esc_html($planSubtitle) : '&nbsp;') . '</div>';
        echo '</div>';

        // Price.
        echo '<div class="impulse-pricing-price" style="padding:14px 20px;text-align:center;transform:translateZ(' . (int) $zPrice . 'px)">';
        if ($planOriginalPrice !== '') {
            echo '<div style="text-decoration:line-through;color:' . esc_attr($cardTextColor) . ';font-size:14px;margin-bottom:4px">' . esc_html($planCurrency . $planOriginalPrice) . '</div>';
        }
        echo '<div class="impulse-price-display">';
        echo '<span class="impulse-price-monthly">';
        echo '<span class="impulse-price-currency" style="font-size:20px;vertical-align:top;color:' . esc_attr($cardPriceColor) . ';font-weight:500">' . esc_html($planCurrency) . '</span>';
        echo '<span class="impulse-price-amount" style="font-size:' . (int) $priceSize . 'px;font-weight:600;color:' . esc_attr($cardPriceColor) . ';line-height:1;letter-spacing:-0.02em">' . esc_html($planPrice) . '</span>';
        echo '<span class="impulse-price-period" style="font-size:14px;color:' . esc_attr($cardTextColor) . ';font-weight:400">' . esc_html($planPeriod) . '</span>';
        echo '</span>';
        if ($showToggle) {
            echo '<span class="impulse-price-yearly" style="display:none">';
            echo '<span class="impulse-price-currency" style="font-size:20px;vertical-align:top;color:' . esc_attr($cardPriceColor) . ';font-weight:500">' . esc_html($planCurrency) . '</span>';
            echo '<span class="impulse-price-amount" style="font-size:' . (int) $priceSize . 'px;font-weight:600;color:' . esc_attr($cardPriceColor) . ';line-height:1;letter-spacing:-0.02em">' . esc_html($planYearlyPrice) . '</span>';
            echo '<span class="impulse-price-period" style="font-size:14px;color:' . esc_attr($cardTextColor) . ';font-weight:400">' . esc_html($planYearlyPeriod) . '</span>';
            echo '</span>';
        }
        echo '</div></div>';

        // Features list.
        echo '<div class="impulse-pricing-features" style="padding:0 20px 20px;flex:1;transform:translateZ(' . (int) $zFeatures . 'px)">';
        foreach ($planFeatures as $feature) {
            $featureText   = is_string($feature) ? (string) $feature : (string) ($feature['text'] ?? '');
            $featureStatus = is_string($feature) ? 'included'        : (string) ($feature['status'] ?? 'included');
            if ($featureStatus === 'included') {
                $iconColor = $isHighlighted ? $highlightCheckColor : $checkColor;
                $iconSvg   = function_exists('impulse_get_feature_icon') ? impulse_get_feature_icon($checkIcon, $iconColor, 'impulse-feature-check') : '';
            } elseif ($featureStatus === 'excluded') {
                $iconColor = $isHighlighted ? $highlightCrossColor : $crossColor;
                $iconSvg   = function_exists('impulse_get_feature_icon') ? impulse_get_feature_icon($crossIcon, $iconColor, 'impulse-feature-cross') : '';
            } else {
                $iconColor = $isHighlighted ? $highlightColor : $textColor;
                $iconSvg   = function_exists('impulse_get_feature_icon') ? impulse_get_feature_icon('minus-square', $iconColor, 'impulse-feature-neutral') : '';
            }
            echo '<div class="impulse-pricing-feature" style="display:flex;align-items:flex-start;gap:12px;margin-bottom:14px;">';
            echo '<span style="flex-shrink:0;display:flex;align-items:center;height:21px">';
            echo wp_kses($iconSvg, ['svg' => ['class' => [], 'viewbox' => [], 'width' => [], 'height' => [], 'fill' => [], 'stroke' => [], 'stroke-width' => [], 'stroke-linecap' => [], 'stroke-linejoin' => []], 'path' => ['d' => []], 'polyline' => ['points' => []], 'line' => ['x1' => [], 'y1' => [], 'x2' => [], 'y2' => []], 'circle' => ['cx' => [], 'cy' => [], 'r' => []], 'rect' => ['x' => [], 'y' => [], 'width' => [], 'height' => [], 'rx' => [], 'ry' => []]]);
            echo '</span>';
            echo '<span style="color:' . esc_attr($isHighlighted ? $highlightColor : $textColor) . ';font-size:14px;line-height:21px">' . esc_html($featureText) . '</span>';
            echo '</div>';
        }
        echo '</div>';

        // Button.
        $btnStyle  = 'display:block;text-align:center;padding:' . $btnPaddingV . 'px 24px;border-radius:' . $borderRadius . 'px;text-decoration:none;font-weight:500;font-size:' . $btnFontSize . 'px;transition:all 0.2s;';
        $btnStyle .= 'background:' . esc_attr($cardBtnBg) . ';color:' . esc_attr($cardBtnColor) . ';border:none;';
        echo '<div style="padding:0 20px 20px;transform:translateZ(' . (int) $zButton . 'px)">';
        echo '<a href="' . esc_url($buttonHref) . '" class="impulse-pricing-button" style="' . esc_attr($btnStyle) . '">' . esc_html($planButtonText) . '</a>';
        echo '</div>';

        echo '</div>'; // .impulse-pricing-card
    }
    echo '</div>'; // .impulse-pricing-grid
    echo '</div>'; // .impulse-pricing-wrapper

    // ── Arc parallax JS (cursor + gyro + axis switch) ────────────
    // Frontend's richer JS includes mobile vertical fan + gyroscope.
    // Now emitted in both contexts (editor preview gains gyro support
    // when running on a touch device, which is a no-op on desktop).
    ob_start();
    echo '(function(){'
        . 'var grid=document.getElementById(' . wp_json_encode($uniqueId) . ');'
        . 'if(!grid)return;var row=grid.querySelector(".impulse-pricing-grid");'
        . 'if(!row)return;'
        . 'var cards=row.querySelectorAll(":scope > .impulse-pricing-card");'
        . 'if(cards.length<2)return;'
        . 'function readBase(){return Array.prototype.map.call(cards,function(c){return parseFloat(c.getAttribute("data-arc-base"))||0;});}'
        . 'var base=readBase();var MAX=3,dx=0,dy=0,tdx=0,tdy=0,easing=false,hov=new Array(cards.length).fill(false);'
        // isArc respects the mobile opt-out: on viewports ≤768px the
        // wrapper must carry data-mobile-arc="1" for the arc to apply.
        // Without it, CSS resets card transforms to none and the JS skip
        // here keeps apply() from re-stamping them every frame.
        . 'function isArc(){'
        .   'if(!row.classList.contains("impulse-pricing-arc"))return false;'
        .   'if(window.innerWidth<=768){return grid.getAttribute("data-mobile-arc")==="1";}'
        .   'return true;'
        . '}'
        . 'function isVertical(){return window.innerWidth<=768;}'
        . 'function arcScaleY(i){var w=cards[i].offsetWidth||0;if(!w)return 1;var s=Math.abs(Math.sin(base[i]*Math.PI/180));return 1+(w*s)/2400;}'
        . 'function arcScaleX(i){var h=cards[i].offsetHeight||0;if(!h)return 1;var s=Math.abs(Math.sin(base[i]*Math.PI/180));return 1+(h*s)/2400;}'
        . 'function apply(){var on=isArc();var vert=isVertical();cards.forEach(function(c,i){if(!on){c.style.transform="";c.style.filter="";c.removeAttribute("data-arc-hover");return;}var absBase=Math.abs(base[i]);if(hov[i]){c.style.transform=vert?"rotateX(0deg) rotateY(0deg) scaleX(1)":"rotateY(0deg) rotateX(0deg) scaleY(1)";c.style.filter="none";c.setAttribute("data-arc-hover","1");}else{if(vert){var sx=arcScaleX(i);c.style.transform="rotateX("+(base[i]-dy).toFixed(2)+"deg) rotateY("+dx.toFixed(2)+"deg) scaleX("+sx.toFixed(4)+")";}else{var sy=arcScaleY(i);c.style.transform="rotateY("+(base[i]+dx).toFixed(2)+"deg) rotateX("+(-dy).toFixed(2)+"deg) scaleY("+sy.toFixed(4)+")";}var b=absBase<0.5?0:Math.min(2.5,0.4+absBase/14);c.style.filter=b>0?"blur("+b.toFixed(2)+"px)":"none";c.removeAttribute("data-arc-hover");}});}'
        . 'function ease(){dx+=(tdx-dx)*0.06;dy+=(tdy-dy)*0.06;apply();if(Math.abs(tdx-dx)>0.01||Math.abs(tdy-dy)>0.01){requestAnimationFrame(ease);}else{easing=false;}}'
        . 'function kickEase(){if(!easing){easing=true;requestAnimationFrame(ease);}}'
        . 'row.addEventListener("mousemove",function(e){if(!isArc())return;var r=row.getBoundingClientRect();tdx=((e.clientX-r.left)/r.width-0.5)*(MAX*2);tdy=((e.clientY-r.top)/r.height-0.5)*(MAX*2);kickEase();});'
        . 'row.addEventListener("mouseleave",function(){tdx=0;tdy=0;kickEase();});'
        . 'cards.forEach(function(c,i){c.addEventListener("mouseenter",function(){hov[i]=true;apply();});c.addEventListener("mouseleave",function(){hov[i]=false;apply();});});'
        . 'if("ontouchstart" in window){cards.forEach(function(c,i){c.addEventListener("touchstart",function(){for(var k=0;k<hov.length;k++)hov[k]=false;hov[i]=true;apply();},{passive:true});});row.addEventListener("touchstart",function(e){if(!e.target.closest(".impulse-pricing-card")){for(var k=0;k<hov.length;k++)hov[k]=false;apply();}},{passive:true});}'
        . 'function bindGyro(){window.addEventListener("deviceorientation",function(e){if(!isArc())return;var g=e.gamma||0,b=e.beta||0;tdx=Math.max(-MAX,Math.min(MAX,g/15));tdy=Math.max(-MAX,Math.min(MAX,(b-30)/15));kickEase();});}'
        . 'if(typeof DeviceOrientationEvent!=="undefined"&&typeof DeviceOrientationEvent.requestPermission==="function"){var onFirstTouch=function(){DeviceOrientationEvent.requestPermission().then(function(r){if(r==="granted")bindGyro();}).catch(function(){});};document.addEventListener("touchstart",onFirstTouch,{once:true});}else if("ontouchstart" in window){bindGyro();}'
        . 'window.addEventListener("resize",function(){apply();});'
        . 'row._impulseArcRefresh=function(){base=readBase();apply();};'
        . 'document.addEventListener("impulse-screen-changed",function(){setTimeout(function(){var host=row.closest(\'.impulse-screen,.impulse-el[data-type="section"]\');var anyActive=document.querySelector(\'.impulse-screen-active\');if(host&&anyActive&&!host.classList.contains("impulse-screen-active"))return;apply();},50);});'
        . 'apply();'
        . '})();';
    $_inlineJs = ob_get_clean();
    wp_add_inline_script('instinctor-frontend-js', $_inlineJs, 'after');

    // ── Frontend-only responsive @media for size sliders ─────────
    if (!$isEditor && function_exists('impulse_responsive_css') && function_exists('wp_add_inline_style')) {
        $pResp  = impulse_responsive_css($settings, '#' . $uniqueId . ' .impulse-pricing-card',   ['border_radius' => ['border-radius', 'px']]);
        $pResp .= impulse_responsive_css($settings, '#' . $uniqueId . ' .impulse-price-amount',   ['price_size'    => ['font-size',     'px']]);
        $pResp .= impulse_responsive_css($settings, '#' . $uniqueId . ' .impulse-pricing-button', ['btn_font_size' => ['font-size',     'px']]);
        if ($pResp) wp_add_inline_style('instinctor-page-inline', $pResp);
    }

    // ── Pricing toggle JS ────────────────────────────────────────
    if ($showToggle) {
        wp_add_inline_script('instinctor-frontend-js', '(function(){var w=document.getElementById("' . esc_js($uniqueId) . '");if(!w)return;var t=w.querySelector(".impulse-billing-toggle");if(!t)return;t.addEventListener("change",function(){var yearly=this.checked;w.querySelectorAll(".impulse-price-monthly").forEach(function(el){el.style.display=yearly?"none":""});w.querySelectorAll(".impulse-price-yearly").forEach(function(el){el.style.display=yearly?"":"none"});var ml=w.querySelector(".impulse-toggle-monthly"),yl=w.querySelector(".impulse-toggle-yearly"),knob=w.querySelector(".impulse-toggle-knob"),bg=knob?knob.parentElement.previousElementSibling:null;if(ml)ml.style.opacity=yearly?"0.5":"1";if(yl)yl.style.opacity=yearly?"1":"0.5";if(knob)knob.style.left=yearly?"27px":"3px";if(bg)bg.style.background=yearly?"#1d2327":"#ddd"})})();', 'after');
    }

    return ob_get_clean();
}
