<?php
declare(strict_types=1);

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

/**
 * Flip Box element renderer (shared).
 *
 * Phase 1, step 25. A two-sided card with rich hover-animation
 * variants: flip / spring / cube / push / zoom / slide / morph /
 * glitch / stack / depth-rise / void /
 * fold / swing. Each animation has a CSS setup branch and a JS
 * handler that runs on hover (frontend) or stays inert (editor
 * preview is markup-only — hover doesn't actually flip in the
 * builder).
 *
 * Pre-extraction differences:
 *
 *   - Editor honoured a `padding` uniform-slider setting that took
 *     priority over the 4-side per-direction values. Frontend
 *     always used the 4-side values. Unified to the editor's
 *     priority logic — `padding` (uniform) wins when set, else
 *     the 4-side stack.
 *
 *   - Frontend bundled the entire hover-handler JS (~250 lines)
 *     inline at the end. Editor emits markup only and lets the
 *     user see static state in the canvas.
 *
 *   - Frontend wraps with `isolation:isolate` on outer + container
 *     so 3D transforms don't bleed onto neighbors; editor doesn't.
 *
 *   - Visibility classes (hide-mobile / hide-tablet) frontend only.
 */
function impulse_element_render_flip_box($el, array $settings, string $context): string {
    if (is_object($el)) $el = (array) $el;
    $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;
    };

    // ── Content settings ─────────────────────────────────────────
    $frontTitle    = (string) ($settings['front_title']    ?? '');
    $frontContent  = (string) ($settings['front_content']  ?? '');
    $frontBg       = (string) ($settings['front_bg']       ?? '#f5f5f5');
    $frontColor    = (string) ($settings['front_color']    ?? '#1d2327');
    $frontTitleSize   = intval($read('front_title_size',   24));
    $frontContentSize = intval($read('front_content_size', 14));
    $backTitle     = (string) ($settings['back_title']     ?? '');
    $backContent   = (string) ($settings['back_content']   ?? '');
    $backButtonText  = (string) ($settings['back_button_text']  ?? '');
    $backButtonLink  = (string) ($settings['back_button_link']  ?? '#');
    $backButtonBg    = (string) ($settings['back_button_bg']    ?? 'rgba(255,255,255,0.2)');
    $backButtonColor = (string) ($settings['back_button_color'] ?? '');
    $backBg        = (string) ($settings['back_bg']        ?? '#e0e0e0');
    $backColor     = (string) ($settings['back_color']     ?? '#1d2327');

    // bg fallback chain — empty/transparent → parent section bg → page bg
    $secBg  = (string) ($GLOBALS['impulse_current_section_bg'] ?? '');
    $pageBg = (string) ($GLOBALS['impulse_page_bg_color']      ?? '#ffffff');
    $bgFallback = ($secBg !== '' && $secBg !== 'transparent') ? $secBg : $pageBg;
    if ($frontBg === '' || $frontBg === 'transparent') $frontBg = $bgFallback;
    if ($backBg  === '' || $backBg  === 'transparent') $backBg  = $bgFallback;

    $backTitleSize    = intval($settings['back_title_size']    ?? $frontTitleSize);
    $backContentSize  = intval($settings['back_content_size']  ?? $frontContentSize);
    $animationType    = (string) ($settings['animation_type']  ?? 'spring');
    // Default 900ms reads as premium-cinematic; 600 felt rushed.
    $animSpeed        = intval($settings['animation_speed']    ?? 900);

    $flipDirection  = (string) $read('flip_direction',  'horizontal');
    $slideDirection = (string) $read('slide_direction', 'left');
    $height        = intval($read('height',        250));
    $borderRadius  = intval($read('border_radius', 8));

    /* Per-device dimensions (frontend). The Height and Max Width
       sliders on the panel are responsive — they write to
       height_tablet / height_mobile / section_max_width_tablet /
       section_max_width_mobile when the active device tab isn't
       desktop. Before this block the frontend ignored those keys
       and emitted the desktop value at every viewport; the per-
       device controls felt broken. Read explicit per-device values
       here so we can emit @media overrides further below. Null =
       "designer didn't set", fall through to desktop on render. */
    $heightTablet = isset($settings['height_tablet']) && $settings['height_tablet'] !== ''
        ? intval($settings['height_tablet']) : null;
    $heightMobile = isset($settings['height_mobile']) && $settings['height_mobile'] !== ''
        ? intval($settings['height_mobile']) : null;
    $maxWidthTablet = isset($settings['section_max_width_tablet']) && $settings['section_max_width_tablet'] !== ''
        ? intval($settings['section_max_width_tablet']) : null;
    $maxWidthMobile = isset($settings['section_max_width_mobile']) && $settings['section_max_width_mobile'] !== ''
        ? intval($settings['section_max_width_mobile']) : null;

    // Padding: uniform slider takes priority over 4-side when set.
    $uniformPadding = isset($settings['padding']) ? intval($read('padding', 0)) : null;
    if ($uniformPadding !== null) {
        $padding = $uniformPadding . 'px';
    } else {
        $paddingTop    = intval($settings['padding_top']    ?? 24);
        $paddingRight  = intval($settings['padding_right']  ?? 24);
        $paddingBottom = intval($settings['padding_bottom'] ?? 24);
        $paddingLeft   = intval($settings['padding_left']   ?? 24);
        $padding = $paddingTop . 'px ' . $paddingRight . 'px ' . $paddingBottom . 'px ' . $paddingLeft . 'px';
    }

    // Visibility classes (frontend only).
    $visClass = (!$isEditor && function_exists('impulse_get_visibility_classes'))
        ? impulse_get_visibility_classes($settings) : '';

    // ── Per-side border ─────────────────────────────────────────
    $fbBorderWidth = intval($settings['border_width'] ?? 0);
    $fbBorderColor = esc_attr((string) ($settings['border_color'] ?? '#e0e0e0'));
    $fbBorderStyle = '';
    if ($fbBorderWidth > 0) {
        $fbBorderTop    = !isset($settings['border_top'])    || $settings['border_top']    !== '0';
        $fbBorderRight  = !isset($settings['border_right'])  || $settings['border_right']  !== '0';
        $fbBorderBottom = !isset($settings['border_bottom']) || $settings['border_bottom'] !== '0';
        $fbBorderLeft   = !isset($settings['border_left'])   || $settings['border_left']   !== '0';
        $bw = $fbBorderWidth . 'px';
        $fbBorderStyle = 'border-top:'    . ($fbBorderTop    ? $bw . ' solid ' . $fbBorderColor : 'none') . ';'
                       . 'border-right:'  . ($fbBorderRight  ? $bw . ' solid ' . $fbBorderColor : 'none') . ';'
                       . 'border-bottom:' . ($fbBorderBottom ? $bw . ' solid ' . $fbBorderColor : 'none') . ';'
                       . 'border-left:'   . ($fbBorderLeft   ? $bw . ' solid ' . $fbBorderColor : 'none') . ';';
    }

    // ── Base side style ─────────────────────────────────────────
    // Vertical alignment — top / center / bottom. Top reads as a
    // premium spec card (consistent baseline across cards regardless
    // of text length); center is the legacy behavior.
    $_vAlignMap = ['top' => 'flex-start', 'center' => 'center', 'bottom' => 'flex-end'];
    $fbVAlign   = $_vAlignMap[(string)($settings['vertical_align'] ?? 'top')] ?? 'flex-start';
    // Horizontal content alignment (v2.1.1828). Default 'center' keeps the
    // legacy look; 'left'/'right' set align-items + text-align so titles,
    // descriptions, and the optional icon line up on that edge.
    $_hAlignMap = ['left' => 'flex-start', 'center' => 'center', 'right' => 'flex-end'];
    $fbHAlign     = (string)($settings['content_align'] ?? 'center');
    $fbAlignItems = $_hAlignMap[$fbHAlign] ?? 'center';
    $fbTextAlign  = in_array($fbHAlign, ['left', 'center', 'right'], true) ? $fbHAlign : 'center';
    $baseSideStyle = 'position:absolute;inset:0;display:flex;flex-direction:column;align-items:' . $fbAlignItems . ';justify-content:' . $fbVAlign . ';padding:' . $padding . ';text-align:' . $fbTextAlign . ';border-radius:' . $borderRadius . 'px;transition:all 0.6s ease-in-out;' . $fbBorderStyle;

    // Frosted glass — applied to the OUTER card container, NOT the faces.
    // Browsers reliably skip backdrop-filter when the element is inside a
    // transform-style:preserve-3d ancestor (the .impulse-flip-inner has
    // preserve-3d for spring/cube). Putting the filter on the container
    // (which lives above the 3D context) makes the blur actually paint.
    // The faces sit on top with translucent bg as the tint layer.
    $frostedGlass = !empty($settings['frosted_glass']);

    $fbMaxWidth = intval($settings['section_max_width'] ?? 600);
    // Spring rotates the card through 3D space with perspective:1200px.
    // During the overshoot bounce (cubic-bezier(0.4, 0, 0.2, 1) sweeping
    // past 180deg) the card's projected silhouette extends slightly beyond
    // the front-facing rectangle — overflow:hidden on the container clipped
    // those edges and the flip looked cropped. Skip the base overflow rule
    // for spring; modes that genuinely translate outside (stack, slide,
    // push, cube, depth-rise) add overflow:hidden in their
    // own case branches below.
    //
    // isolation:isolate is added on the frontend to keep front/back z-index
    // stacking contained inside the card — but it creates a new stacking
    // context that backdrop-filter cannot sample outside of (the frost has
    // nothing to blur). Drop it when frosted_glass is on so backdrop-filter
    // can see the page environment behind the card.
    $containerStyle = 'width:100%;'
                    . ($fbMaxWidth > 0 ? 'max-width:' . $fbMaxWidth . 'px;' : '')
                    . 'height:' . $height . 'px;position:relative;'
                    . ($animationType === 'spring' ? '' : 'overflow:hidden;')
                    . ((!$isEditor && !$frostedGlass) ? 'isolation:isolate;' : '');
    if ($frostedGlass) {
        // Container is the only place backdrop-filter survives the
        // preserve-3d ancestor. Give it border-radius so the blur is
        // rounded; the faces still own per-side tint via their own bg.
        $containerStyle .= 'backdrop-filter:blur(20px) saturate(180%);'
                         . '-webkit-backdrop-filter:blur(20px) saturate(180%);'
                         . 'border-radius:' . $borderRadius . 'px;';
    }
    $innerStyle = 'position:relative;width:100%;height:100%;';
    $frontStyle = $baseSideStyle . 'background:' . esc_attr($frontBg) . ';color:' . esc_attr($frontColor) . ';';
    // Editor: back face ignores pointer events so clicking the flipped card can't
    // land on the back (which reflowed/repositioned its bottom-aligned text on click);
    // the click passes through to select the element normally. Frontend keeps the back
    // interactive so back buttons still work.
    $backStyle  = $baseSideStyle . 'background:' . esc_attr($backBg) . ';color:' . esc_attr($backColor) . ';' . ($isEditor ? 'pointer-events:none;' : '');

    $dataAttrs = 'data-animation="' . esc_attr($animationType) . '" data-speed="' . $animSpeed . '"';

    // ── Per-animation CSS setup ─────────────────────────────────
    switch ($animationType) {
        case 'fade':
            // No flip: the front simply fades out to reveal the back (which sits
            // underneath at full opacity). Used by demo team cards where the back is
            // the same portrait under a dark scrim, so the bio appears on a dark
            // overlay without the card flipping. The inner is preserve-3d (for the
            // flip effect), where z-index is ignored — so a tiny translateZ keeps the
            // front physically in front; opacity drives the reveal. Sergei 2026-07-13.
            $frontStyle .= 'z-index:2;transform:translateZ(1px);';
            $backStyle  .= 'z-index:1;';
            break;
        case 'zoom':
            $frontStyle .= 'z-index:2;';
            $backStyle  .= 'z-index:1;transform:scale(0.82);opacity:0;';
            break;
        case 'slide':
            $containerStyle .= 'overflow:hidden;';
            $frontStyle .= 'z-index:3;';
            $backStyle  .= 'z-index:1;opacity:0;';
            $dataAttrs  .= ' data-slide-direction="' . esc_attr($slideDirection) . '"';
            break;
        case 'push':
            $containerStyle .= 'overflow:hidden;';
            $innerStyle .= 'transition:transform 0.6s ease-in-out;';
            $pushOffsets = ['left'=>'transform:translateX(100%);','right'=>'transform:translateX(-100%);','up'=>'transform:translateY(100%);','down'=>'transform:translateY(-100%);'];
            $backStyle .= $pushOffsets[$slideDirection] ?? $pushOffsets['left'];
            $dataAttrs .= ' data-slide-direction="' . esc_attr($slideDirection) . '" data-push="1"';
            break;
        case 'morph':
            $frontStyle .= 'z-index:2;clip-path:circle(150% at 50% 50%);transition:clip-path 0.55s cubic-bezier(0.4,0,0.2,1);';
            $backStyle  .= 'z-index:1;';
            break;
        case 'glitch':
            $frontStyle .= 'z-index:2;transition:none;';
            $backStyle  .= 'z-index:1;opacity:0;transition:opacity 0.3s ease-in-out;';
            break;
        case 'fold':
        case 'flip':
        case 'swing':
        case 'stack':
            $frontStyle .= 'z-index:2;';
            $backStyle  .= 'z-index:1;opacity:0;transform:scale(0.92);';
            break;
        case 'cube':
            $containerStyle .= 'perspective:1000px;overflow:hidden;';
            $innerStyle .= 'transform-style:preserve-3d;transition:transform 0.6s ease-in-out;';
            $cubeTranslate = $height / 2;
            $frontStyle .= 'backface-visibility:hidden;-webkit-backface-visibility:hidden;transform:translateZ(' . $cubeTranslate . 'px);';
            $backStyle  .= 'backface-visibility:hidden;-webkit-backface-visibility:hidden;transform:rotateX(-90deg) translateZ(' . $cubeTranslate . 'px);';
            $dataAttrs  .= ' data-cube-translate="' . $cubeTranslate . '"';
            break;
        case 'depth-rise':
            $containerStyle .= 'perspective:600px;overflow:hidden;';
            $frontStyle .= 'z-index:2;';
            $backStyle  .= 'z-index:1;opacity:0;';
            break;
        case 'void':
            $frontStyle .= 'z-index:2;';
            $backStyle  .= 'z-index:1;transform:scale(0);opacity:0;';
            break;
        // ─── New-generation effects (v2.1.764) ──────────────────────
        // Cursor-driven reveals — the legacy effects all run on a
        // fixed enter/leave clock and reveal back as one global state
        // swap. These use cursor POSITION as part of the effect
        // itself, which the older 2D-era effects can't do.
        case 'spotlight':
            // Front has a circular feathered HOLE that follows the cursor,
            // drilling through to the back behind it. Back is always
            // opaque underneath. The reveal is LOCAL (only where you
            // point) and continuous, not a discrete state swap. No
            // perspective needed — flat 2D mask.
            $containerStyle .= 'overflow:hidden;';
            $frontStyle     .= 'z-index:2;';
            $backStyle      .= 'z-index:1;';
            break;
        case 'xray-scan':
            // Bright vertical bar sweeps left → right across the card.
            // Behind the bar, front is progressively clipped away by
            // clip-path inset(), revealing back as the scan progresses.
            // One-shot enter animation; reverses on leave. Sci-fi /
            // photocopy reveal that no other effect mimics.
            // border-radius on the container so `overflow:hidden` clips
            // the scan bar to the rounded card shape — without it the
            // bar shows past the corners where the rounded face ends
            // but the rectangular container clip box continues.
            $containerStyle .= 'overflow:hidden;border-radius:' . $borderRadius . 'px;';
            $frontStyle     .= 'z-index:2;clip-path:inset(0 0 0 0);';
            $backStyle      .= 'z-index:1;';
            break;

        case 'spring':
        default:
            $containerStyle .= 'perspective:1200px;';
            $innerStyle .= 'transform-style:preserve-3d;';
            $flipTransform = $flipDirection === 'vertical' ? 'rotateX(180deg)' : 'rotateY(180deg)';
            $frontStyle .= 'backface-visibility:hidden;-webkit-backface-visibility:hidden;';
            $backStyle  .= 'backface-visibility:hidden;-webkit-backface-visibility:hidden;transform:' . $flipTransform . ';';
            $dataAttrs  .= ' data-flip-direction="' . esc_attr($flipDirection) . '"';
            break;
    }

    // ── Compose HTML ────────────────────────────────────────────
    $flipClass = 'impulse-flip-box';
    if ($visClass !== '') $flipClass .= ' ' . $visClass;

    /* Per-instance id for the @media overrides. Designer-set
       tablet/mobile values for Height + Max Width emit a scoped
       inline-style block keyed on this id (via wp_add_inline_style).
       The global mobile rule in frontend.css that used to clamp every
       flipbox to height:auto + min-height:170px is dropped in this
       release, so the designer's actual sliders drive the per-device dimensions.
       Skip the emit in editor (already device-switches via $read). */
    $elId = isset($el['id']) ? (string) $el['id'] : '';
    $fbInstanceId = ($elId !== '') ? 'impulse-fb-' . preg_replace('/[^a-zA-Z0-9_-]/', '', $elId) : '';
    $idAttr = $fbInstanceId !== '' ? ' id="' . esc_attr($fbInstanceId) . '"' : '';

    if (!$isEditor && $fbInstanceId !== '') {
        $tabletCss = '';
        if ($heightTablet !== null)   $tabletCss .= 'height:' . $heightTablet . 'px;';
        if ($maxWidthTablet !== null) $tabletCss .= 'max-width:' . ($maxWidthTablet > 0 ? $maxWidthTablet . 'px' : 'none') . ';';
        $mobileCss = '';
        if ($heightMobile !== null)   $mobileCss .= 'height:' . $heightMobile . 'px;';
        if ($maxWidthMobile !== null) $mobileCss .= 'max-width:' . ($maxWidthMobile > 0 ? $maxWidthMobile . 'px' : 'none') . ';';
        if ($tabletCss !== '' || $mobileCss !== '') {
            $fbInline = '';
            if ($tabletCss !== '') {
                $fbInline .= '@media (max-width:1024px){#' . $fbInstanceId . '{' . $tabletCss . '}}';
            }
            if ($mobileCss !== '') {
                $fbInline .= '@media (max-width:767px){#' . $fbInstanceId . '{' . $mobileCss . '}}';
            }
            if (function_exists('wp_add_inline_style')) {
                wp_add_inline_style('instinctor-page-inline', $fbInline);
            } else {
                wp_add_inline_style('instinctor-page-inline', $fbInline);
            }
        }
    }

    /* One-time mobile rule: icon_position=left renders a horizontal
       [icon | text] row that's cramped on a phone. On <=767px stack it to a
       centered column (icon above the text), everything centered. Echoed
       (not wp_add_inline_style) so it also lands in the editor preview, and
       shares its guard with the inline renderer in impulse-page.php so the
       page emits it exactly once regardless of which path drew the boxes. */
    if (empty($GLOBALS['impulse_flip_iconrow_css'])) {
        $GLOBALS['impulse_flip_iconrow_css'] = true;
        wp_add_inline_style('instinctor-page-inline', '@media (max-width:767px){'
            . '.impulse-flip-iconrow{flex-direction:column!important;align-items:center!important;gap:10px!important}'
            . '.impulse-flip-iconrow>div{align-items:center!important;text-align:center!important}'
            . '}');
    }

    ob_start();
    // Outer wrapper: skip isolation:isolate when frosted_glass is on so the
    // card's backdrop-filter can sample the page environment behind it. With
    // isolation set, the frost has nothing to blur.
    $outerStyle = ($isEditor || $frostedGlass)
        ? 'display:flex;justify-content:center;width:100%;margin:0 auto;align-self:center'
        : 'display:flex;justify-content:center;width:100%;margin:0 auto;align-self:center;isolation:isolate';
    echo '<div style="' . esc_attr($outerStyle) . '">';
    echo '<div class="' . esc_attr($flipClass) . '"' . wp_kses($idAttr, impulse_svg_allowed()) . ' style="' . esc_attr($containerStyle) . '" ' . wp_kses($dataAttrs, impulse_svg_allowed()) . '>';
    echo '<div class="impulse-flip-inner" style="' . esc_attr($innerStyle) . '">';

    // Front side. impulse_keep_last_words() binds the last two words
    // of each line with a non-breaking space — bulletproof orphan
    // protection that doesn't depend on text-wrap:pretty being honored.
    $_kw = function_exists('impulse_keep_last_words') ? 'impulse_keep_last_words' : function ($s) { return $s; };
    echo '<div class="impulse-flip-front" style="' . esc_attr($frontStyle) . '">';
    // Optional Lucide icon (v2.1.1828). front_icon = a Lucide name; icon
    // inherits the front text color. icon_position (v2.1.1831): 'top'
    // stacks it above the title (default); 'left' places it beside the
    // title+content as a compact horizontal row, the icon centered against
    // the two text lines.
    $frontIcon = (string) ($settings['front_icon'] ?? '');
    $iconPos   = (string) ($settings['icon_position'] ?? 'top');
    $_fiSvg = '';
    if ($frontIcon !== '' && function_exists('impulse_lucide_svg')) {
        $_fiSize = intval($settings['front_icon_size'] ?? 24);
        $_fiSvg  = impulse_lucide_svg($frontIcon, $_fiSize, 'currentColor');
    }
    $titleMarginF = ($frontContent !== '' || $isEditor) ? 'margin-bottom:4px;' : '';
    $_titleStyleF   = 'font-size:' . $frontTitleSize . 'px;font-weight:600;line-height:1.2;text-wrap:pretty;' . $titleMarginF;
    $_titleInnerF   = function_exists('impulse_kses_richtext') ? impulse_kses_richtext($_kw($frontTitle)) : esc_html($_kw($frontTitle));
    $_contentStyleF = 'font-size:' . $frontContentSize . 'px;line-height:1.3;text-wrap:pretty;opacity:0.7';
    $_contentInnerF = function_exists('impulse_kses_richtext') ? impulse_kses_richtext($_kw($frontContent)) : esc_html($_kw($frontContent));
    if ($iconPos === 'left' && $_fiSvg !== '') {
        // Horizontal: icon column + text column. align-items:center keeps the
        // icon vertically centered against the title+description block.
        echo '<div class="impulse-flip-iconrow" style="display:flex;flex-direction:row;align-items:center;gap:12px;width:100%">';
        echo   '<div class="impulse-flip-icon" style="flex:0 0 auto;line-height:0;opacity:0.9">' . wp_kses($_fiSvg, impulse_svg_allowed()) . '</div>';
        echo   '<div style="display:flex;flex-direction:column;min-width:0">';
        echo     '<div class="impulse-flip-title" style="' . esc_attr($_titleStyleF) . '">' . wp_kses_post($_titleInnerF) . '</div>';
        echo     '<div class="impulse-flip-content" style="' . esc_attr($_contentStyleF) . '">' . wp_kses_post($_contentInnerF) . '</div>';
        echo   '</div>';
        echo '</div>';
    } else {
        if ($_fiSvg !== '') {
            echo '<div class="impulse-flip-icon" style="line-height:0;margin-bottom:10px;opacity:0.9">' . wp_kses($_fiSvg, impulse_svg_allowed()) . '</div>';
        }
        echo '<div class="impulse-flip-title" style="' . esc_attr($_titleStyleF) . '">' . wp_kses_post($_titleInnerF) . '</div>';
        echo '<div class="impulse-flip-content" style="' . esc_attr($_contentStyleF) . '">' . wp_kses_post($_contentInnerF) . '</div>';
    }
    echo '</div>';

    // Back side.
    echo '<div class="impulse-flip-back" style="' . esc_attr($backStyle) . '">';
    $titleMarginB = ($backContent !== '' || $backButtonText !== '' || $isEditor) ? 'margin-bottom:12px;' : '';
    $_backTitleInner   = function_exists('impulse_kses_richtext') ? impulse_kses_richtext($_kw($backTitle)) : esc_html($_kw($backTitle));
    $_backContentInner = function_exists('impulse_kses_richtext') ? impulse_kses_richtext($_kw($backContent)) : esc_html($_kw($backContent));
    echo '<div class="impulse-flip-title" style="' . esc_attr('font-size:' . $backTitleSize . 'px;font-weight:600;text-wrap:pretty;' . $titleMarginB) . '">' . wp_kses_post($_backTitleInner) . '</div>';
    echo '<div class="impulse-flip-content" style="' . esc_attr('font-size:' . $backContentSize . 'px;text-wrap:pretty;') . '">' . wp_kses_post($_backContentInner) . '</div>';
    if ($backButtonText !== '') {
        $_btnBg    = $backButtonBg;
        $_btnColor = $backButtonColor !== '' ? $backButtonColor : 'inherit';
        echo '<a href="' . esc_url($backButtonLink) . '" class="impulse-flip-button" style="display:inline-block;margin-top:16px;padding:10px 24px;background:' . esc_attr($_btnBg) . ';color:' . esc_attr($_btnColor) . ';border-radius:6px;text-decoration:none;">' . esc_html($backButtonText) . '</a>';
    }
    echo '</div>';

    echo '</div></div></div>';

    // ── Frontend hover-handler JS (substantial; editor stays inert) ──
    if (!$isEditor) {
        ob_start();
        echo '(function(){
if(!window._impulseFlipBoxRegistry)window._impulseFlipBoxRegistry=[];
function _enh(box){
    var inner=box.querySelector(".impulse-flip-inner"),front=box.querySelector(".impulse-flip-front"),back=box.querySelector(".impulse-flip-back");
    if(!inner||!front||!back)return;
    var a=box.dataset.animation||"flip";
    window._impulseFlipBoxRegistry.push(box);
    var sh=null;
    var ts=0,tx=0,ty=0;
    box.addEventListener("touchstart",function(e){ts=Date.now();tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:true});
    box.addEventListener("touchend",function(e){
        var dt=Date.now()-ts,dx=e.changedTouches[0].clientX-tx,dy=e.changedTouches[0].clientY-ty,dist=Math.sqrt(dx*dx+dy*dy);
        if(dist>28||dt>650)return; // tap flips; a >28px swipe is a scroll/screen-nav gesture (ignore), long-press >650ms ignored. Prior <28 required a swipe so plain taps never flipped on mobile.
        if(!box._touchFlipped){box._touchFlipped=true;box.dispatchEvent(new MouseEvent("mouseenter",{bubbles:true,cancelable:true}))}
        else{box._touchFlipped=false;box.dispatchEvent(new MouseEvent("mouseleave",{bubbles:true,cancelable:true}))}
    });
}
document.querySelectorAll(".impulse-flip-box").forEach(_enh);
if(!window._impulseGyroInit&&("ontouchstart" in window||navigator.maxTouchPoints>0)){
    window._impulseGyroInit=true;
    function _applyGyro(e){
        var ry=(Math.max(-28,Math.min(28,e.gamma||0))/28)*18,rx=-(Math.max(-20,Math.min(20,(e.beta||45)-45))/20)*12;
        (window._impulseFlipBoxRegistry||[]).forEach(function(fb){
            if(fb._touchFlipped||fb._tiltActive)return;
            var rect=fb.getBoundingClientRect();
            if(rect.bottom<0||rect.top>window.innerHeight)return;
            var fi=fb.querySelector(".impulse-flip-inner");if(!fi)return;
            fi.style.transition="transform 0.15s ease-in-out";fi.style.transform="rotateY("+ry+"deg) rotateX("+rx+"deg)";
        });
    }
    function _startG(){if(typeof DeviceOrientationEvent!=="undefined"){if(typeof DeviceOrientationEvent.requestPermission==="function"){DeviceOrientationEvent.requestPermission().then(function(s){if(s==="granted")window.addEventListener("deviceorientation",_applyGyro)}).catch(function(){})}else{window.addEventListener("deviceorientation",_applyGyro)}}}
    document.addEventListener("touchstart",function _gr(){_startG();document.removeEventListener("touchstart",_gr)},{once:true,passive:true});
}
if(!document.getElementById("impulse-tease-css")){var _ts=document.createElement("style");_ts.id="impulse-tease-css";_ts.textContent="@media (pointer:coarse){@keyframes _ift{0%,100%{transform:perspective(600px) rotateY(0deg)}30%{transform:perspective(600px) rotateY(18deg)}60%{transform:perspective(600px) rotateY(-5deg)}}.impulse-flip-box.impulse-tease .impulse-flip-inner{animation:_ift 0.9s cubic-bezier(0.4, 0, 0.2, 1) 1 forwards}}";document.head.appendChild(_ts)}
setTimeout(function(){if(!("ontouchstart" in window)&&navigator.maxTouchPoints<=0)return;document.querySelectorAll(".impulse-flip-box").forEach(function(fb){if(fb._touchFlipped)return;fb.classList.add("impulse-tease");setTimeout(function(){fb.classList.remove("impulse-tease")},1000)})},1200);
})();';
        $_inlineJs1 = ob_get_clean();
        wp_add_inline_script('instinctor-frontend-js', $_inlineJs1, 'after');
        ob_start();
        echo '(function(){
var boxes=document.querySelectorAll(".impulse-flip-box");
if(!boxes.length)return;
function _rb(back){back.style.transition="none";back.style.transitionDelay="0s";back.style.opacity="0";back.style.zIndex="1";setTimeout(function(){back.style.transition="";back.style.transitionDelay=""},20)}
boxes.forEach(function(b){
var inner=b.querySelector(".impulse-flip-inner"),front=b.querySelector(".impulse-flip-front"),back=b.querySelector(".impulse-flip-back");
if(!inner||!front||!back)return;
var a=b.dataset.animation||"spring",fd=b.dataset.flipDirection||"horizontal",sd=b.dataset.slideDirection||"left",spd=parseInt(b.dataset.speed)||600,ct=parseFloat(b.dataset.cubeTranslate)||125;
var shim=null;function _sh(ms){}
var _vx=50,_vy=50;
if(a==="void")b.addEventListener("mousemove",function(e){var r=b.getBoundingClientRect();_vx=((e.clientX-r.left)/r.width*100).toFixed(1);_vy=((e.clientY-r.top)/r.height*100).toFixed(1)});
// Spring — smooth 3D flip. Earlier overshoot curve removed (Y > 1
// bounced past 180° and read as toy-ish). Material-standard
// ease-in-out on both directions for a premium feel.
if(a==="spring"){
  b.addEventListener("mouseenter",function(){
    inner.style.transition="transform "+(spd/1000)+"s cubic-bezier(0.4,0,0.2,1)";
    inner.style.transform=fd==="vertical"?"rotateX(180deg)":"rotateY(180deg)";
  });
  b.addEventListener("mouseleave",function(){
    inner.style.transition="transform "+(spd/1000)+"s cubic-bezier(0.4,0,0.2,1)";
    inner.style.transform=""
  });
  return
}
// spotlight — cursor drills a feathered hole through front, revealing back.
// Radius scales to card size (55% of the smaller dim) so it covers a real
// portion of the card regardless of dimensions, not a tiny pinhole.
if(a==="spotlight"){
  var _spR=0,_spTR=0,_spX="50%",_spY="50%",_spRaf=null,_spMax=0,_spFeather=60;
  function _spDraw(){
    var inR=Math.max(0,_spR-_spFeather);
    var m="radial-gradient(circle "+_spR+"px at "+_spX+" "+_spY+",transparent 0,transparent "+inR+"px,black "+_spR+"px,black 100%)";
    front.style.webkitMaskImage=m;front.style.maskImage=m;
  }
  function _spTick(){
    _spR+=(_spTR-_spR)*0.22;
    if(Math.abs(_spTR-_spR)<0.3){_spR=_spTR;_spRaf=null;if(_spR===0){front.style.webkitMaskImage="";front.style.maskImage="";return}}
    else _spRaf=requestAnimationFrame(_spTick);
    _spDraw();
  }
  b.addEventListener("mouseenter",function(e){
    var r=b.getBoundingClientRect();
    _spMax=Math.min(r.width,r.height)*0.55;
    _spFeather=Math.max(40,_spMax*0.30);
    _spX=(e.clientX-r.left)+"px";_spY=(e.clientY-r.top)+"px";
    _spTR=_spMax;
    if(!_spRaf)_spRaf=requestAnimationFrame(_spTick);
  });
  b.addEventListener("mousemove",function(e){
    if(_spR<1&&_spTR<1)return;
    var r=b.getBoundingClientRect();
    _spX=(e.clientX-r.left)+"px";_spY=(e.clientY-r.top)+"px";
    _spDraw();
  });
  b.addEventListener("mouseleave",function(){
    _spTR=0;
    if(!_spRaf)_spRaf=requestAnimationFrame(_spTick);
  });
  return
}
// xray-scan — bright vertical bar sweeps left→right; front is clipped
// in its wake by clip-path inset(), revealing back. Reverses on leave.
if(a==="xray-scan"){
  if(!b._xrayBar){
    var _xb=document.createElement("div");
    _xb.className="impulse-xray-bar";
    _xb.style.cssText="position:absolute;top:-6%;bottom:-6%;width:8px;background:linear-gradient(to right,rgba(255,255,255,0),rgba(255,255,255,0.96),rgba(255,255,255,0));box-shadow:0 0 28px 6px rgba(255,255,255,0.7),0 0 60px 10px rgba(255,255,255,0.32);pointer-events:none;z-index:10;left:-6px;opacity:0;will-change:left,opacity";
    inner.appendChild(_xb);
    b._xrayBar=_xb;
  }
  var _xbar=b._xrayBar;
  b.addEventListener("mouseenter",function(){
    var s=(spd/1000);
    front.style.transition="clip-path "+s+"s cubic-bezier(0.55,0.05,0.4,1)";
    void front.offsetWidth;
    front.style.clipPath="inset(0 0 0 100%)";
    _xbar.style.transition="opacity 0.18s ease-in-out,left "+s+"s cubic-bezier(0.55,0.05,0.4,1)";
    _xbar.style.opacity="1";
    _xbar.style.left="calc(100% - 4px)";
  });
  b.addEventListener("mouseleave",function(){
    var s=(spd*0.75/1000);
    front.style.transition="clip-path "+s+"s cubic-bezier(0.5,0,0.3,1)";
    front.style.clipPath="inset(0 0 0 0)";
    _xbar.style.transition="left "+s+"s cubic-bezier(0.5,0,0.3,1),opacity 0.18s ease-in-out "+(s*0.6)+"s";
    _xbar.style.left="-6px";
    _xbar.style.opacity="0";
  });
  return
}
b.addEventListener("mouseenter",function(){
if(a==="cube"){inner.style.transform="rotateX(90deg)"}
else if(a==="fade"){front.style.transition="opacity "+(spd*0.55/1000)+"s ease-in-out";front.style.opacity="0"}
else if(a==="push"){var pm={left:"translateX(-100%)",right:"translateX(100%)",up:"translateY(-100%)",down:"translateY(100%)"};inner.style.transform=pm[sd]||pm.left}
else if(a==="zoom"){front.style.transition="transform "+(spd*0.45/1000)+"s ease-in-out,opacity "+(spd*0.4/1000)+"s ease-in-out";front.style.transform="scale(0)";front.style.opacity="0";back.style.transition="transform "+(spd*0.5/1000)+"s cubic-bezier(0.4, 0, 0.2, 1) "+(spd*0.18/1000)+"s,opacity "+(spd*0.4/1000)+"s ease-in-out "+(spd*0.18/1000)+"s";back.style.transform="scale(1)";back.style.opacity="1";front.style.zIndex="1";back.style.zIndex="2"}
else if(a==="slide"){front.style.opacity="0";front.style.transition="opacity 0.3s ease-in-out";var _ih=(sd==="left"||sd==="right");inner.querySelectorAll(".impulse-shutter-strip").forEach(function(s){s.style.transform=_ih?"scaleX(0)":"scaleY(0)";s.style.opacity="0"});setTimeout(function(){back.style.opacity="1"},spd*0.4)}
else if(a==="morph"){void front.offsetHeight;front.style.clipPath="circle(0% at 50% 50%)"}
else if(a==="glitch"){front.classList.add("impulse-flip-glitching");b._gt=setTimeout(function(){front.style.opacity="0";back.style.opacity="1";front.style.zIndex="1";back.style.zIndex="2"},500)}
else if(a==="stack"){front.style.transition="transform "+(spd*0.38/1000)+"s ease-in-out,opacity "+(spd*0.3/1000)+"s ease-in-out";front.style.transform="translateX(55%) rotate(4deg) scale(0.9)";front.style.opacity="0";back.style.transition="transform "+(spd*0.35/1000)+"s cubic-bezier(0.4, 0, 0.2, 1) "+(spd*0.22/1000)+"s,opacity "+(spd*0.3/1000)+"s ease-in-out "+(spd*0.22/1000)+"s";back.style.opacity="1";back.style.transform="scale(1)";front.style.zIndex="1";back.style.zIndex="2"}
else if(a==="depth-rise"){front.style.transition="transform "+(spd*0.38/1000)+"s cubic-bezier(0.4,0,1,1),opacity "+(spd*0.32/1000)+"s ease-in-out "+(spd*0.08/1000)+"s";front.style.transform="translateZ(420px)";front.style.opacity="0";front.style.zIndex="1";back.style.zIndex="2";setTimeout(function(){back.style.transition="opacity 0.38s ease-in-out";back.style.opacity="1"},spd*0.28)}
else if(a==="void"){var vo=_vx+"% "+_vy+"%";front.style.transformOrigin=vo;back.style.transformOrigin=vo;front.style.transition="transform "+(spd*0.42/1000)+"s cubic-bezier(0.4,0,1,1),opacity "+(spd*0.38/1000)+"s ease-in-out";front.style.transform="scale(0)";front.style.opacity="0";back.style.transition="transform "+(spd*0.5/1000)+"s cubic-bezier(0.4, 0, 0.2, 1) "+(spd*0.25/1000)+"s,opacity "+(spd*0.4/1000)+"s ease-in-out "+(spd*0.25/1000)+"s";back.style.transform="scale(1)";back.style.opacity="1";front.style.zIndex="1";back.style.zIndex="2"}
});
b.addEventListener("mouseleave",function(){
if(a==="cube"||a==="push"){inner.style.transition="transform 0.5s ease-in-out";inner.style.transform=""}
else if(a==="fade"){front.style.opacity=""}
else if(a==="zoom"){_rb(back);back.style.transform="scale(0.82)";front.style.transition="";front.style.transform="";front.style.opacity="";front.style.zIndex="2"}
else if(a==="slide"){_rb(back);front.style.opacity="";front.style.transition=""}
else if(a==="morph"){front.style.transition="clip-path "+(spd*0.55/1000)+"s cubic-bezier(0.4,0,0.2,1)";front.style.clipPath="circle(150% at 50% 50%)"}
else if(a==="glitch"){if(b._gt)clearTimeout(b._gt);front.classList.remove("impulse-flip-glitching");front.style.opacity="";front.style.zIndex="2";_rb(back)}
else if(a==="stack"){_rb(back);back.style.transform="scale(0.92)";front.style.transition="";front.style.transform="";front.style.opacity="";front.style.zIndex="2"}
else if(a==="depth-rise"){_rb(back);front.style.transition="none";front.style.transform="";front.style.opacity="";front.style.zIndex="2"}
else if(a==="void"){_rb(back);back.style.transform="scale(0)";front.style.transformOrigin="";front.style.transition="";front.style.transform="";front.style.opacity="";front.style.zIndex="2"}
});
});
})();';
        $_inlineJs2 = ob_get_clean();
        wp_add_inline_script('instinctor-frontend-js', $_inlineJs2, 'after');
    }

    return ob_get_clean();
}
