<?php
declare(strict_types=1);

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

/**
 * Popup (modal / drawer / toast) element renderer (shared).
 *
 * Phase 1, step 15.
 *
 * Position vocabulary (canonical, used in both contexts):
 *   center            → centred dialog with explicit width
 *   center-left/right → full-height vertical drawer (off-canvas side)
 *   top/bottom-center → full-width horizontal banner
 *   top/bottom-left/right → corner toast (16px gutter, sized by popup_width)
 *
 * Edge effect vocabulary (`overlay_edge_effect` setting):
 *   none      (default)
 *   glow      → soft outward + inset glow following the popup's bg colour
 *   specular  → animated light strip travelling along the popup edge
 *   caustic   → soft randomised highlight blobs
 *   pressure  → strong directional shadow + inset highlight
 *
 * Pre-extraction:
 *   - Editor used `impulse-modal-scrim` wrapper (display:none until
 *     ".selected" on the editor wrapper) + a chip button so the
 *     author can see the popup exists without it covering the page.
 *   - Frontend used `.impulse-modal` wrapper with the trigger
 *     data-attrs (data-trigger, data-delay, data-scroll, data-once,
 *     etc.) that the runtime JS reads to open the popup at the right
 *     moment.
 *   - The big duplicated logic — position sizing math + edge effect
 *     CSS — was byte-identical in both copies (270+ lines). That's
 *     what this extraction consolidates.
 */
function impulse_element_render_modal($el, array $settings, array $children, string $context, callable $renderChild): string {
    if (is_object($el)) $el = (array) $el;
    $elId     = preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($el['id'] ?? '0'));
    $isEditor = ($context === 'editor');
    $device   = $isEditor ? ($GLOBALS['impulse_preview_device'] ?? 'desktop') : 'desktop';

    $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;
    };

    // ── Settings ──────────────────────────────────────────────────
    $popupBg      = (string) ($settings['popup_bg'] ?? '#ffffff');
    $popupRadius  = intval($read('popup_radius',  0));
    // v2.1.1383: Padding split into H + V axes. `popup_padding` (legacy
    // single value) seeds both when the new keys are unset, so modals
    // saved before this version render exactly the same.
    $_legacyPad   = intval($read('popup_padding', 24));
    $popupPadH    = intval($read('popup_padding_h', $_legacyPad));
    $popupPadV    = intval($read('popup_padding_v', $_legacyPad));
    // Composite for `padding:V H` shorthand.
    $popupPaddingShorthand = $popupPadV . 'px ' . $popupPadH . 'px';
    $popupWidth   = intval($read('popup_width',  500));
    $popupHeight  = intval($read('popup_height',   0));
    // v2.1.1382: Content Gap — space between children inside the modal.
    // Was hardcoded to 8px in editor + missing on frontend.
    $popupContentGap = max(0, intval($read('popup_content_gap', 8)));
    $overlayColor = (string) ($settings['overlay_color'] ?? 'rgba(0,0,0,0.5)');
    // Per-device overlay: useful for drawer-style popups where the desktop
    // popup floats over plenty of free space (transparent overlay reads as
    // "the form is part of the scene"), but on mobile the popup covers
    // most of the viewport and needs a darkening backdrop so the form
    // doesn't sit on top of legible content underneath. Unset → fall back
    // to the desktop value. Sergei 2026-06-02.
    $overlayColorTablet = (string) ($settings['overlay_color_tablet'] ?? $overlayColor);
    $overlayColorMobile = (string) ($settings['overlay_color_mobile'] ?? $overlayColor);
    $showClose    = ($settings['show_close'] ?? true) !== false
                    && ($settings['show_close'] ?? null) !== '0'
                    && ($settings['show_close'] ?? null) !== 0;
    $position     = (string) ($settings['position'] ?? 'center');
    // Per-device position. Desktop value is canonical; tablet + mobile
    // are optional — drawers that pin to the right on desktop usually
    // want to centre on mobile so the form isn't cramped against an edge.
    $positionTablet = (string) ($settings['position_tablet'] ?? $position);
    $positionMobile = (string) ($settings['position_mobile'] ?? $position);
    // Per-device bg image. Sergei 2026-06-02: hiding the portrait on mobile
    // produces a cleaner centred form (no side image to fight with).
    $hideBgImageTablet = !empty($settings['popup_bg_image_hide_tablet']);
    $hideBgImageMobile = !empty($settings['popup_bg_image_hide_mobile']);

    // ── Position + size math ─────────────────────────────────────
    // Compose a closure so the same math can run for desktop / tablet /
    // mobile values when per-device position settings are present.
    $_positionMath = function(string $pos) use ($popupWidth, $popupRadius, $popupHeight, $isEditor) {
        if      ($pos === 'center')        { return ['top:50%;left:50%;transform:translate(-50%,-50%)', 'width:' . $popupWidth . 'px;' . ($isEditor ? 'height:auto;' : '') . 'max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);', 'border-radius:' . $popupRadius . 'px;']; }
        elseif  ($pos === 'center-left')   { return ['top:0;left:0;bottom:0',                            'width:' . $popupWidth . 'px;height:auto!important;max-width:100vw;',   'border-radius:0 ' . $popupRadius . 'px ' . $popupRadius . 'px 0;']; }
        elseif  ($pos === 'center-right')  { return ['top:0;right:0;bottom:0',                           'width:' . $popupWidth . 'px;height:auto!important;max-width:100vw;',   'border-radius:' . $popupRadius . 'px 0 0 ' . $popupRadius . 'px;']; }
        elseif  ($pos === 'top-center')    { return ['top:0;left:0;right:0',                             'width:auto!important;max-width:none!important;' . ($popupHeight > 0 ? 'height:' . $popupHeight . 'px;' : 'min-height:200px;'), 'border-radius:0 0 ' . $popupRadius . 'px ' . $popupRadius . 'px;']; }
        elseif  ($pos === 'bottom-center') { return ['bottom:0;left:0;right:0',                          'width:auto!important;max-width:none!important;' . ($popupHeight > 0 ? 'height:' . $popupHeight . 'px;' : 'min-height:200px;'), 'border-radius:' . $popupRadius . 'px ' . $popupRadius . 'px 0 0;']; }
        elseif  (in_array($pos, ['top-left','top-right','bottom-left','bottom-right'], true)) {
            $yEdge = strpos($pos, 'top-') === 0 ? 'top:16px' : 'bottom:16px';
            $xEdge = substr($pos, -5) === '-left' ? 'left:16px' : 'right:16px';
            return [$yEdge . ';' . $xEdge, 'width:' . $popupWidth . 'px;' . ($isEditor ? 'height:auto;' : '') . 'max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);', 'border-radius:' . $popupRadius . 'px;'];
        }
        return ['top:50%;left:50%;transform:translate(-50%,-50%)', 'width:' . $popupWidth . 'px;', 'border-radius:' . $popupRadius . 'px;'];
    };
    [$containerPos, $sizeStyle, $radiusStyle] = $_positionMath($position);
    $heightStyle = ($position === 'center' && $popupHeight > 0) ? 'min-height:' . $popupHeight . 'px;' : '';

    // ── Edge effect CSS ──────────────────────────────────────────
    $edgeFx  = (string) ($settings['overlay_edge_effect'] ?? 'none');
    $edgeCss = '';
    if ($edgeFx !== 'none') {
        $edgeOf = 'perimeter';
        if      ($position === 'center-left')   $edgeOf = 'right';
        elseif  ($position === 'center-right')  $edgeOf = 'left';
        elseif  ($position === 'top-center')    $edgeOf = 'bottom';
        elseif  ($position === 'bottom-center') $edgeOf = 'top';

        if ($edgeFx === 'glow') {
            if ($edgeOf === 'perimeter') {
                $edgeCss = 'position:absolute;inset:0;box-shadow:0 0 60px 8px ' . esc_attr($popupBg) . ',0 0 140px 20px ' . esc_attr($popupBg) . ',inset 0 0 30px rgba(255,255,255,0.12);opacity:0.9;border-radius:inherit;pointer-events:none;z-index:0';
            } else {
                $shadowOff = ['right'=>'20px 0 60px 10px','left'=>'-20px 0 60px 10px','top'=>'0 -20px 60px 10px','bottom'=>'0 20px 60px 10px'][$edgeOf];
                $edgeCss = 'position:absolute;inset:0;box-shadow:' . $shadowOff . ' ' . esc_attr($popupBg) . ';opacity:0.9;border-radius:inherit;pointer-events:none;z-index:0';
            }
        } elseif ($edgeFx === 'specular') {
            $isVert = in_array($edgeOf, ['left','right','perimeter'], true);
            $axis = $isVert ? 'Y' : 'X';
            $insetPos = ['right'=>'right:0;top:0;bottom:0;width:2px','left'=>'left:0;top:0;bottom:0;width:2px','top'=>'top:0;left:0;right:0;height:2px','bottom'=>'bottom:0;left:0;right:0;height:2px','perimeter'=>'right:0;top:0;bottom:0;width:2px'][$edgeOf];
            $stripDir = $isVert ? '180deg' : '270deg';
            $bgSize   = $isVert ? '100% 40%' : '40% 100%';
            $bgPos    = $isVert ? '0 -50%' : '-50% 0';
            $edgeCss = 'position:absolute;' . $insetPos . ';overflow:hidden;background-image:linear-gradient(' . $stripDir . ',transparent 0%,rgba(255,255,255,0.85) 45%,rgba(255,255,255,1) 50%,rgba(255,255,255,0.85) 55%,transparent 100%);background-size:' . $bgSize . ';background-repeat:no-repeat;background-position:' . $bgPos . ';animation:impulseOvSpecular' . $axis . ' 3s ease-in-out infinite;pointer-events:none;z-index:0';
        } elseif ($edgeFx === 'caustic') {
            $insetPos2 = ['right'=>'right:0;top:0;bottom:0;width:40px','left'=>'left:0;top:0;bottom:0;width:40px','top'=>'top:0;left:0;right:0;height:30px','bottom'=>'bottom:0;left:0;right:0;height:30px','perimeter'=>'inset:0'][$edgeOf];
            $edgeCss = 'position:absolute;' . $insetPos2 . ';background:radial-gradient(circle at 20% 30%,rgba(255,255,255,0.35) 0%,transparent 40%),radial-gradient(circle at 80% 70%,rgba(255,255,255,0.25) 0%,transparent 35%),radial-gradient(circle at 50% 90%,rgba(255,255,255,0.3) 0%,transparent 40%);mix-blend-mode:screen;opacity:0.6;pointer-events:none;z-index:0';
        } elseif ($edgeFx === 'pressure') {
            if ($edgeOf === 'perimeter') {
                $edgeCss = 'position:absolute;inset:0;box-shadow:0 30px 90px -10px rgba(0,0,0,0.55),inset 0 1px 0 rgba(255,255,255,0.35);border-radius:inherit;pointer-events:none;z-index:0';
            } else {
                $outerShadow = ['right'=>'30px 0 60px -8px','left'=>'-30px 0 60px -8px','top'=>'0 -30px 60px -8px','bottom'=>'0 30px 60px -8px'][$edgeOf];
                $innerHl     = ['right'=>'inset -1px 0 0 rgba(255,255,255,0.25)','left'=>'inset 1px 0 0 rgba(255,255,255,0.25)','top'=>'inset 0 -1px 0 rgba(255,255,255,0.25)','bottom'=>'inset 0 1px 0 rgba(255,255,255,0.25)'][$edgeOf];
                $edgeCss = 'position:absolute;inset:0;box-shadow:' . $outerShadow . ' rgba(0,0,0,0.55),' . $innerHl . ';border-radius:inherit;pointer-events:none;z-index:0';
            }
        }
    }

    // ── Optional decorative backdrop image ─────────────────────────
    // `popup_bg_image` lets a popup carry a transparent cut-out (a
    // person, an illustration) grounded at the bottom edge of the
    // container, behind any child content. Same construction as the
    // editor-demo Modal>Right-drawer sample: absolute, full width,
    // height capped at `popup_bg_image_height`% (default 50) so the
    // figure sits in the lower half and form/text children remain
    // readable on top. pointer-events:none so it never intercepts
    // clicks. The popup-content div above it has z-index:1, so
    // children naturally paint on top — no per-child z-index needed.
    $popupBgImage       = trim((string) ($settings['popup_bg_image'] ?? ''));
    $popupBgImageHeight = max(10, min(100, intval($settings['popup_bg_image_height'] ?? 50)));
    $bgImageHtml        = '';
    if ($popupBgImage !== '') {
        // Anchor the bg image to the edge the popup is docked to. When the
        // popup is a right-side drawer, the cut-out figure should touch the
        // right edge of the screen (not float centred with whitespace on
        // either side). Mirror logic for left drawers. Top/bottom drawers
        // keep the centred horizontal alignment. Sergei 2026-05-31.
        $_bgObjPos = 'center bottom';
        if ($position === 'center-right' || $position === 'top-right' || $position === 'bottom-right') {
            $_bgObjPos = 'right bottom';
        } elseif ($position === 'center-left' || $position === 'top-left' || $position === 'bottom-left') {
            $_bgObjPos = 'left bottom';
        }
        $bgImageHtml = '<img class="impulse-modal-bg-image" src="' . esc_url($popupBgImage) . '" alt=""'
                     . ' style="position:absolute;left:0;right:0;bottom:0;width:100%;height:' . $popupBgImageHeight
                     . '%;object-fit:contain;object-position:' . $_bgObjPos . ';z-index:0;pointer-events:none;user-select:none;display:block">';
    }

    // Specular keyframes — emit once per page (singleton).
    if ($edgeFx === 'specular' && empty($GLOBALS['impulse_ov_specular_css_emitted']) && function_exists('wp_add_inline_style')) {
        $GLOBALS['impulse_ov_specular_css_emitted'] = true;
        // Frontend uses background-position transitions; editor used different keyframe
        // values but for the same effect. Unify to the frontend's (which used "transform"
        // until 2.1.x — current spec uses background-position).
        wp_add_inline_style('instinctor-page-inline', '@keyframes impulseOvSpecularY{0%{background-position:0 -50%}100%{background-position:0 150%}}@keyframes impulseOvSpecularX{0%{background-position:-50% 0}100%{background-position:150% 0}}');
    }

    // ── Editor: chip + scrim wrapper, no trigger data attrs ──────
    ob_start();
    if ($isEditor) {
        // Chip — minimised marker in page flow so the popup is visible in the
        // builder canvas. CSS rule elsewhere flips the scrim visible when the
        // editor wrapper has ".selected".
        echo '<button type="button" class="impulse-modal-chip" style="display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;background:#1d2327;color:#fff;font:11px/1 -apple-system,BlinkMacSystemFont,sans-serif;border:none;border-radius:3px;cursor:pointer;letter-spacing:0.02em">';
        echo '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="5" width="18" height="14" rx="2"/></svg>Modal</button>';

        echo '<div class="impulse-modal-scrim" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:90;background:' . esc_attr($overlayColor) . '">';
        echo '<div class="impulse-modal-container" data-modal-id="' . esc_attr((string) $elId) . '" style="position:absolute;' . esc_attr($containerPos) . ';' . esc_attr($sizeStyle) . esc_attr($radiusStyle) . 'background:' . esc_attr($popupBg) . ';padding:' . esc_attr($popupPaddingShorthand) . ';box-sizing:border-box;overflow:hidden;display:flex;flex-direction:column;' . esc_attr($heightStyle) . '">';

        if (function_exists('impulse_render_bg_shapes')) {
            impulse_render_bg_shapes($settings, $device, true);
        }
        if ($bgImageHtml) {
            echo '<img class="impulse-modal-bg-image" src="' . esc_url($popupBgImage) . '" alt=""'
               . ' style="position:absolute;left:0;right:0;bottom:0;width:100%;height:' . intval($popupBgImageHeight)
               . '%;object-fit:contain;object-position:' . esc_attr($_bgObjPos) . ';z-index:0;pointer-events:none;user-select:none;display:block">';
        }
        if ($edgeCss) echo '<div class="impulse-modal-edge-fx" style="' . esc_attr($edgeCss) . '"></div>';
        if ($showClose) {
            echo '<button type="button" class="impulse-modal-close-editor" style="position:absolute;top:8px;right:8px;width:28px;height:28px;border:none;background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#999;z-index:2" title="Close modal"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>';
        }
        // v2.1.1385: dropzone — tightened from 160px → 48px min-height,
        // added an explanatory hint, and gated to desktop tab only. The
        // huge empty zone with a single `+` was confusing to designers
        // who didn't know the block was an editor-only drop target.
        echo '<div class="impulse-modal-content" style="position:relative;display:flex;flex-direction:column;gap:' . intval($popupContentGap) . 'px;flex:1;min-height:120px">';
        if (!empty($children)) {
            foreach ($children as $child) $renderChild($child);
        }
        // Dropzone — desktop tab only. Tablet + mobile preview the rendered
        // modal without the editor scaffold so designers see what visitors
        // will see at those breakpoints. v2.1.1386: dropped the legacy
        // `impulse-grid-cell impulse-grid-cell-empty` classes — they brought
        // a display:grid layout AND a `::before` content:'+' pseudo that
        // fought our flex centering AND produced a duplicate plus icon. Keep
        // only the dropzone class so the drag-target handler still matches.
        if ($device === 'desktop') {
            echo '<div class="impulse-modal-dropzone" data-parent="' . esc_attr((string) $elId) . '" style="position:relative;flex:0 0 auto;min-height:44px;display:flex;align-items:center;justify-content:center;gap:8px;padding:6px 14px;border:1px dashed rgba(130,130,130,0.35);border-radius:4px;background:rgba(130,130,130,0.04);box-sizing:border-box">';
            echo '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="rgba(130,130,130,0.6)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;pointer-events:none"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
            echo '<span style="font-size:11px;color:rgba(130,130,130,0.75);pointer-events:none;line-height:1.3;white-space:nowrap">Drop blocks here. Editor only, not rendered on frontend.</span>';
            echo '</div>';
        }
        echo '</div></div></div>';
        return ob_get_clean();
    }

    // ── Frontend: real popup with trigger data attrs ─────────────
    $GLOBALS['impulse_needs_popup'] = true;

    $trigger        = (string) ($settings['trigger']         ?? 'click');
    $triggerDelay   = intval($settings['trigger_delay']      ?? 5);
    $triggerScroll  = intval($settings['trigger_scroll']     ?? 50);
    $oncePerSession = !empty($settings['once_per_session']);
    $closeOnOverlay = ($settings['close_on_overlay'] ?? true) !== false;
    $animation      = (string) ($settings['popup_animation']  ?? 'fade');
    $buttonSelector = (string) ($settings['button_selector']  ?? '');
    /* Auto-dismiss — closes the popup after `auto_dismiss_after`
       seconds without manual interaction. The flag that turns
       "corner popup" into a real toast. Off by default so existing
       popups don't suddenly start auto-closing. Timer starts on
       showPopup, clears on hidePopup or close-button click so a
       visitor who reads quickly doesn't get hijacked. */
    $autoDismiss      = !empty($settings['auto_dismiss']);
    $autoDismissAfter = max(1, intval($settings['auto_dismiss_after'] ?? 4));

    // Honour the user-editable popup_id field; fall back to auto.
    $rawPopupId = !empty($settings['popup_id']) ? (string) $settings['popup_id'] : ('popup-' . ($elId ?: (function_exists('wp_rand') ? wp_rand(1000, 9999) : random_int(1000, 9999))));
    $popupId    = preg_replace('/[^A-Za-z0-9_-]/', '', $rawPopupId);
    if ($popupId === '') $popupId = 'popup-' . ($elId ?: random_int(1000, 9999));

    echo '<div id="' . esc_attr($popupId) . '" class="impulse-modal"'
       . ' data-trigger="'        . esc_attr($trigger) . '"'
       . ' data-delay="'          . intval($triggerDelay) . '"'
       . ' data-scroll="'         . intval($triggerScroll) . '"'
       . ' data-once="'           . ($oncePerSession ? '1' : '0') . '"'
       . ' data-close-overlay="'  . ($closeOnOverlay ? '1' : '0') . '"'
       . ' data-button="'         . esc_attr($buttonSelector) . '"'
       . ' data-position="'       . esc_attr($position) . '"'
       . ' data-animation="'      . esc_attr($animation) . '"'
       . ' data-auto-dismiss="'   . ($autoDismiss ? '1' : '0') . '"'
       . ' data-dismiss-after="'  . intval($autoDismissAfter) . '"'
       /* z-index above the header (z:1000) so drawers don't sit
          UNDER the header chrome — visitor reports "top of drawer hidden
          behind header" came from the overlay's stacking order. 100100
          beats any sane chrome value while staying well below 2^31.
          Sergei 2026-05-31. */
       . ' style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;z-index:100100;background:' . esc_attr($overlayColor) . '">';
    echo '<div class="impulse-modal-container" style="position:absolute;' . esc_attr($containerPos) . ';' . esc_attr($sizeStyle) . 'overflow:hidden;background:' . esc_attr($popupBg) . ';' . esc_attr($radiusStyle) . 'padding:' . esc_attr($popupPaddingShorthand) . ';box-sizing:border-box;' . esc_attr($heightStyle) . '">';

    if (function_exists('impulse_render_bg_shapes')) {
        impulse_render_bg_shapes($settings, 'desktop', false);
    }
    if ($bgImageHtml) {
        echo '<img class="impulse-modal-bg-image" src="' . esc_url($popupBgImage) . '" alt=""'
           . ' style="position:absolute;left:0;right:0;bottom:0;width:100%;height:' . intval($popupBgImageHeight)
           . '%;object-fit:contain;object-position:' . esc_attr($_bgObjPos) . ';z-index:0;pointer-events:none;user-select:none;display:block">';
    }
    if ($edgeCss) echo '<div class="impulse-modal-edge-fx" style="' . esc_attr($edgeCss) . '"></div>';
    if ($showClose) {
        echo '<button type="button" class="impulse-modal-close" style="position:absolute;top:8px;right:8px;width:28px;height:28px;border:none;background:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#999;z-index:2"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>';
    }
    echo '<div class="impulse-modal-content" style="position:relative;z-index:1;display:flex;flex-direction:column;gap:' . intval($popupContentGap) . 'px">';
    foreach ($children as $child) $renderChild($child);
    echo '</div></div></div>';

    $html = ob_get_clean();

    // Responsive @media — width/padding/radius/height overrides per device.
    if (function_exists('impulse_responsive_css') && function_exists('wp_add_inline_style')) {
        $resp = impulse_responsive_css($settings, '#' . $popupId . ' .impulse-modal-container', [
            'popup_width'   => ['width',         'px'],
            'popup_radius'  => ['border-radius', 'px'],
            'popup_height'  => function ($v) { $v = intval($v); return $v > 0 ? 'min-height:' . $v . 'px;' : 'min-height:0;'; },
        ]);
        if ($resp) wp_add_inline_style('instinctor-page-inline', $resp);

        // v2.1.1383: per-device padding — read H + V axes per breakpoint and
        // emit a single `padding: V H` shorthand override. Falls back through
        // the new keys' device suffixes, then the legacy `popup_padding` keys
        // (per device, then base), then nothing.
        $_padSel = '#' . $popupId . ' .impulse-modal-container';
        $_padCss = '';
        $_padForDevice = function($dev) use ($settings, $popupPadH, $popupPadV) {
            $sufH = isset($settings['popup_padding_h_' . $dev]) ? intval($settings['popup_padding_h_' . $dev]) : null;
            $sufV = isset($settings['popup_padding_v_' . $dev]) ? intval($settings['popup_padding_v_' . $dev]) : null;
            $sufLegacy = isset($settings['popup_padding_' . $dev]) ? intval($settings['popup_padding_' . $dev]) : null;
            if ($sufH === null && $sufV === null && $sufLegacy === null) return null;
            $h = $sufH !== null ? $sufH : ($sufLegacy !== null ? $sufLegacy : $popupPadH);
            $v = $sufV !== null ? $sufV : ($sufLegacy !== null ? $sufLegacy : $popupPadV);
            return $v . 'px ' . $h . 'px';
        };
        $_padTab = $_padForDevice('tablet');
        $_padMob = $_padForDevice('mobile');
        if ($_padTab !== null) $_padCss .= '@media(max-width:1024px){' . $_padSel . '{padding:' . $_padTab . '!important}}';
        if ($_padMob !== null) $_padCss .= '@media(max-width:767px){' . $_padSel . '{padding:' . $_padMob . '!important}}';
        if ($_padCss) wp_add_inline_style('instinctor-page-inline', $_padCss);

        // Per-device overlay background override. Desktop value lives inline
        // on the popup root; tablet + mobile go through media queries so a
        // drawer can be transparent on desktop (form floats over scene) and
        // darkened on mobile (form covers most of the viewport, needs scrim
        // so content underneath doesn't bleed through). Sergei 2026-06-02.
        $_ovCss = '';
        if ($overlayColorTablet !== $overlayColor) {
            $_ovCss .= '@media(max-width:1024px){#' . $popupId . '{background:' . esc_attr($overlayColorTablet) . '!important}}';
        }
        if ($overlayColorMobile !== $overlayColor) {
            $_ovCss .= '@media(max-width:767px){#' . $popupId . '{background:' . esc_attr($overlayColorMobile) . '!important}}';
        }

        // Per-device position override. Drawer-on-desktop / centred-on-mobile
        // pattern is common: pin to right on a wide screen, centre on mobile
        // so the form isn't cramped against an edge. Builds a position-style
        // block per breakpoint via the $_positionMath closure (reused) and
        // applies it to the .impulse-modal-container inside the popup.
        // Sergei 2026-06-02.
        if ($positionTablet !== $position) {
            [$cpT, $ssT, $rsT] = $_positionMath($positionTablet);
            // Reset all edge anchors so the new ones don't combine with old.
            $_ovCss .= '@media(max-width:1024px){#' . $popupId . ' .impulse-modal-container{'
                    . 'top:auto!important;right:auto!important;bottom:auto!important;left:auto!important;'
                    . 'transform:none!important;' . esc_attr($cpT) . '!important;'
                    . esc_attr($ssT) . esc_attr($rsT) . '}}';
        }
        if ($positionMobile !== $position) {
            [$cpM, $ssM, $rsM] = $_positionMath($positionMobile);
            $_ovCss .= '@media(max-width:767px){#' . $popupId . ' .impulse-modal-container{'
                    . 'top:auto!important;right:auto!important;bottom:auto!important;left:auto!important;'
                    . 'transform:none!important;' . esc_attr($cpM) . '!important;'
                    . esc_attr($ssM) . esc_attr($rsM) . '}}';
        }

        // Per-device bg-image hiding. Portrait images that anchor to the
        // right edge of a desktop drawer have nowhere to live on a centred
        // mobile popup. Sergei 2026-06-02.
        if ($hideBgImageTablet) {
            $_ovCss .= '@media(max-width:1024px){#' . $popupId . ' .impulse-modal-bg-image{display:none!important}}';
        }
        if ($hideBgImageMobile) {
            $_ovCss .= '@media(max-width:767px){#' . $popupId . ' .impulse-modal-bg-image{display:none!important}}';
        }

        if ($_ovCss) wp_add_inline_style('instinctor-page-inline', $_ovCss);
    }

    return $html;
}
