<?php
declare(strict_types=1);

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

/**
 * Contact form element renderer (shared).
 *
 * Phase 1, step 17. Another "frontend was the prototype, editor got
 * iterated" case — like login + register. Unification adopts the
 * editor's richer version as canonical so the live frontend gains:
 *
 *   - Auto-contrast text against parent_bg / field_bg (frontend
 *     used a simpler one-shot impulse_contrast_color call).
 *   - Placeholder color set via inline CSS that contrasts the field
 *     bg (frontend left it at browser default — visible against
 *     dark fields).
 *   - Field type support: text / email / tel / textarea / checkbox
 *     / select (with newline-separated options). Frontend used to
 *     only handle text / textarea.
 *   - Responsive field width via `width_tablet` / `width_mobile`
 *     data attrs that the form JS reads (frontend ignored these).
 *   - Auto-fallback placeholder uses the type name when the user
 *     clears the label (so fields never render without a hint).
 *
 * Frontend keeps its security extras (wp_nonce_field +
 * Security::honeypotField()).
 *
 * Pre-extraction default drift resolved:
 *   - field_radius: 6 (frontend) / 4 (editor) → 4
 *   - button_bg:    #1d2327 (frontend) / #1c2226 (editor) → #1c2226
 *
 * Section_max_width default: 600 (frontend) / 400 (editor) → 400
 * (editor canonical; matches login/register/checkout defaults).
 *
 * @sideeffect Primes $GLOBALS['impulse_needs_forms'] on frontend;
 *             emits inline placeholder/focus CSS via wp_add_inline_style.
 */
function impulse_element_render_contact($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');

    // Home Services (trades) tune the form into a quote request by default. The
    // initializer stays generic; the real check is stripped from the free build,
    // where $isHomeSvc is always false.
    $isHomeSvc = false;
    

    $formName        = (string) ($settings['form_name'] ?? ($isHomeSvc ? 'Request a Quote' : 'Contact Form'));
    $fieldGap        = intval($settings['field_gap']    ?? 16);
    $fieldRadius     = intval($settings['field_radius'] ?? 4);
    $buttonWidth     = intval($settings['button_width'] ?? 100);
    $formBg          = (string) ($settings['form_bg']           ?? 'transparent');
    $fieldBg         = (string) ($settings['field_bg']          ?? '#f5f5f5');
    $fieldBorderColor = (string) ($settings['field_border_color'] ?? '#e0e0e0');
    $fieldBorderWidth = intval($settings['field_border_width']    ?? 1);
    $fieldTextColor   = (string) ($settings['field_text_color']   ?? '');
    // v2.1.1382: explicit label/placeholder colour. Empty = fall back to
    // auto-contrast defaults so existing forms render identically.
    $fieldLabelColor  = trim((string) ($settings['field_label_color'] ?? ''));
    $onSubmit        = (string) ($settings['on_submit']        ?? 'message');
    $successMessage  = (string) ($settings['success_message']  ?? 'Thank you! Your message has been sent.');
    $redirectUrl     = (string) ($settings['redirect_url']     ?? '');
    $buttonText      = (string) ($settings['button_text']      ?? ($isHomeSvc ? 'Get a Quote' : 'Send Message'));
    $buttonBg        = (string) ($settings['button_bg']        ?? '#1c2226');
    $buttonColor     = (string) ($settings['button_color']     ?? '#ffffff');
    $fmMaxWidth      = intval($settings['section_max_width']  ?? 400);

    if (!$isEditor) $GLOBALS['impulse_needs_forms'] = true;

    // ── Auto-contrast against parent background ──────────────────
    $parentBg = (string) ($GLOBALS['impulse_parent_bg'] ?? ($GLOBALS['impulse_current_section_bg'] ?? ($GLOBALS['impulse_page_bg_color'] ?? '#ffffff')));
    $effectiveBg = ($formBg !== 'transparent' && $formBg !== '') ? $formBg : $parentBg;
    $autoText = function_exists('impulse_contrast_color') ? impulse_contrast_color($effectiveBg) : '#1d2327';
    $isLight = ($autoText === '#111827' || $autoText === '#1d2327');
    $autoTextMid = $isLight ? '#666' : 'rgba(255,255,255,0.6)';

    $fieldAutoText = function_exists('impulse_contrast_color') ? impulse_contrast_color($fieldBg) : '#111827';
    $fieldIsLight  = ($fieldAutoText === '#111827' || $fieldAutoText === '#1d2327');
    $fieldPlaceholder = $fieldIsLight ? '#86868b' : 'rgba(255,255,255,0.45)';

    // Field text — explicit override or auto-contrast.
    $fieldTextOut = $fieldTextColor !== '' ? $fieldTextColor : $fieldAutoText;
    $inputStyle = 'width:100%;padding:12px 16px;background:' . esc_attr($fieldBg) . ';border:' . $fieldBorderWidth . 'px solid ' . esc_attr($fieldBorderColor) . ';border-radius:' . $fieldRadius . 'px;font-size:14px;color:' . esc_attr($fieldTextOut) . ';outline:none;box-sizing:border-box';

    // ── Inline CSS: placeholders + focus ─────────────────────────
    // v2.1.1382: scoped placeholder override when field_label_color is set —
    // emitted for THIS form's wrapper id only, so multiple forms on the same
    // page can each have their own label colour.
    if (function_exists('wp_add_inline_style')) {
        // v2.1.1391: greyscale-only dropdown styling. accent-color + color-scheme
        // force the OS-default blue option highlight into Instinctor's palette
        // where the browser allows; option:checked + option:hover handle the
        // open-list highlight (works in Firefox + some Chromium builds; native
        // OS rendering on Mac may still show system tint, but that's the limit
        // of what CSS can reach inside a native <select> popup).
        // v2.1.1394: also style the custom-dropdown value span (which acts as
        // the placeholder until a value is picked) — match other placeholders'
        // colour from $fieldPlaceholder, and honour the optional Label colour
        // override the same way native placeholders do.
        $_css = '.impulse-form input::placeholder,.impulse-form textarea::placeholder{color:' . $fieldPlaceholder . ';opacity:1!important}'
              . '.impulse-form input:focus::placeholder,.impulse-form textarea:focus::placeholder{color:' . $fieldPlaceholder . '!important;opacity:1!important}'
              . '.impulse-form input:focus,.impulse-form textarea:focus,.impulse-form select:focus{border-color:#888!important;outline:none!important;box-shadow:none!important}'
              . '.impulse-form .impulse-form-dropdown-trigger:focus{border-color:#888!important;outline:none!important;box-shadow:none!important}'
              . '.impulse-form .impulse-form-dropdown-value.is-placeholder{color:' . $fieldPlaceholder . '}'
              . '.impulse-form .impulse-form-dropdown-option:hover{background:#1d2327!important;color:#ffffff!important}'
              . '.impulse-form .impulse-form-dropdown-option.is-active{background:#1d2327;color:#ffffff}';
        // NOTE: the per-form field_label_color placeholder override is NOT
        // added here. wp_add_inline_style targets the page stylesheet, which
        // the editor's single-element refreshElement re-render does NOT carry
        // — so when the user changed the Label colour the form re-rendered but
        // the override never reached the iframe, and labels kept their default
        // (white) colour until a full reload. It's now emitted as an inline
        // <style> WITH the element HTML below ($labelOverrideCss) so it travels
        // with every render — editor live-update, editor reload, and frontend.
        // The rules in $_css above are page-level defaults and persist fine.
        // v2.1.1911.
        wp_add_inline_style('instinctor-page-inline', $_css);
    }

    // ── Fields ───────────────────────────────────────────────────
    $fields = $settings['fields'] ?? null;
    if (is_string($fields)) $fields = json_decode($fields, true);
    if (empty($fields) || !is_array($fields)) {
        $fields = [
            ['type' => 'text',     'label' => 'Your Name',     'name' => 'name',    'required' => true,  'width' => 100],
            ['type' => 'email',    'label' => 'Email Address', 'name' => 'email',   'required' => true,  'width' => 100],
            ['type' => 'textarea', 'label' => 'Message',       'name' => 'message', 'required' => false, 'width' => 100],
        ];
        
    }

    // Per-form label/placeholder colour override — emitted inline with the
    // element (see note above) so it survives the editor's single-element
    // refresh. Scoped to this form's wrapper id, present on editor + frontend.
    if ($fieldLabelColor !== '' && $elId) {
        $_loSel = '#impulse-form-' . intval($elId);
        $labelOverrideCss = $_loSel . ' .impulse-form input::placeholder,'
              . $_loSel . ' .impulse-form textarea::placeholder,'
              . $_loSel . ' .impulse-form select::placeholder,'
              . $_loSel . ' .impulse-form .impulse-form-dropdown-value.is-placeholder{color:' . esc_attr($fieldLabelColor) . '!important;opacity:1!important}';
        wp_add_inline_style('instinctor-page-inline', $labelOverrideCss);
    }

    // ── Compose HTML ─────────────────────────────────────────────
    ob_start();

    $visBadge = ($isEditor && function_exists('impulse_visibility_badge')) ? impulse_visibility_badge($settings) : '';
    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $visBadge is impulse_visibility_badge() output, composed from esc_attr()'d values internally (see helpers.php).
    if ($visBadge) echo '<div style="position:relative">' . wp_kses($visBadge, impulse_svg_allowed());

    $fmMaxStyle = 'width:100%;margin:0 auto;' . ($fmMaxWidth > 0 ? 'max-width:' . $fmMaxWidth . 'px;' : '');
    echo '<div class="impulse-form-wrapper" id="impulse-form-' . intval($elId) . '" style="' . esc_attr($fmMaxStyle) . '">';
    echo '<div style="display:flex;justify-content:var(--impulse-ca,center);width:100%">';

    $formBgStyle = ($formBg !== 'transparent' && $formBg !== '')
        ? 'background:' . $formBg . ';padding:20px;border-radius:' . $fieldRadius . 'px;'
        : '';
    echo '<form class="impulse-form impulse-contact-form" autocomplete="off"'
       . ' data-on-submit="' . esc_attr($onSubmit) . '"'
       . ' data-success-message="' . esc_attr($successMessage) . '"'
       . ' data-success="' . esc_attr($successMessage) . '"'
       . ' data-redirect-url="' . esc_attr($redirectUrl) . '"'
       . ' style="' . esc_attr('position:relative;width:100%;' . $formBgStyle . 'display:flex;flex-wrap:wrap;gap:' . $fieldGap . 'px;color:' . $autoText) . '">';

    // Frontend-only security extras.
    if (!$isEditor) {
        echo '<input type="hidden" name="action" value="instinctor_submit_form">';
        if (function_exists('wp_nonce_field')) wp_nonce_field('instinctor_frontend', 'nonce', true, true);
        if (class_exists('\\Instinctor\\Core\\Security')) echo wp_kses(\Instinctor\Core\Security::honeypotField(), [
            'div'   => ['style' => true, 'aria-hidden' => true],
            'input' => ['type' => true, 'name' => true, 'value' => true, 'tabindex' => true, 'autocomplete' => true],
        ]);
    }

    // Width normalizer — older templates / AI-generated data sometimes ships
    // 'half' / 'full' / 'third' string aliases instead of numeric percentages.
    // intval('half') == 0 silently produced near-zero-width fields (calc(0% - 8px)
    // collapsed every input on the Noir reservation form). Map names → ints
    // before any width math; keep numeric pass-through unchanged.
    $widthAliasMap = ['full' => 100, 'half' => 50, 'third' => 33, 'quarter' => 25, 'two_thirds' => 66, 'two-thirds' => 66];
    $normalizeWidth = function ($raw) use ($widthAliasMap) {
        if (is_int($raw))    return $raw > 0 ? $raw : 100;
        if (is_string($raw)) {
            $key = strtolower(trim($raw));
            if (isset($widthAliasMap[$key])) return $widthAliasMap[$key];
            $n = intval($raw);
            return $n > 0 ? $n : 100;
        }
        return 100;
    };

    foreach ($fields as $idx => $field) {
        $fType  = (string) ($field['type'] ?? 'text');
        $fLabel = (string) ($field['label'] ?? '');
        $fName  = (string) ($field['name']  ?? (function_exists('sanitize_title') ? sanitize_title($fLabel) : strtolower(preg_replace('/[^a-z0-9]+/i', '_', $fLabel))));
        $fRequired = !empty($field['required']);

        // Width — % per-device. Default to 100% if missing.
        $fWidth       = $normalizeWidth($field['width']        ?? 100);
        $fWidthTablet = $normalizeWidth($field['width_tablet'] ?? $fWidth);
        // Mobile default is 100% (single column on phones — the historical
        // behaviour) UNLESS the field explicitly sets width_mobile, which lets
        // chosen fields stay multi-up on phones. Sergei 2026-06-25.
        $fWidthMobile = $normalizeWidth($field['width_mobile'] ?? 100);

        // Placeholder defaults to label, falls back to type if label empty.
        $phLabel = $fLabel !== '' ? $fLabel : ucfirst($fType);
        $placeholder = $phLabel . ($fRequired ? ' *' : '');
        $reqAttr = $fRequired ? ' required' : '';

        // v2.1.1733: render TIME fields as an AM/PM slot dropdown, not a native
        // <input type="time">. The native control's 12h-vs-24h display is dictated
        // by the visitor's OS locale (uncontrollable from HTML), and its submitted
        // value is ALWAYS 24h ("14:30") — so the owner's inquiry email showed 24h
        // regardless. Routing through the existing custom select dropdown
        // guarantees AM/PM in both the picker and the stored/emailed value
        // (e.g. "2:30 PM"). Respects optional min/max (24h "HH:MM") + step (min).
        if ($fType === 'time') {
            $_tStep = (isset($field['step']) && (int) $field['step'] > 0) ? (int) $field['step'] : 30;
            if ($_tStep < 1 || $_tStep > 720) $_tStep = 30;
            $_parseHM = function ($v) {
                if (!is_string($v) || strpos($v, ':') === false) return null;
                $parts = array_map('intval', explode(':', $v));
                return $parts[0] * 60 + ($parts[1] ?? 0);
            };
            $_tMin = 0;
            $_tMax = 24 * 60; // exclusive upper bound (full day)
            if (isset($field['min']) && ($_p = $_parseHM((string) $field['min'])) !== null) $_tMin = $_p;
            if (isset($field['max']) && ($_p = $_parseHM((string) $field['max'])) !== null) $_tMax = $_p + $_tStep; // inclusive
            $_slots = [];
            for ($_m = $_tMin; $_m < $_tMax && $_m < 24 * 60; $_m += $_tStep) {
                $_h = intdiv($_m, 60);
                $_mm = $_m % 60;
                $_ap = $_h < 12 ? 'AM' : 'PM';
                $_h12 = $_h % 12; if ($_h12 === 0) $_h12 = 12;
                $_slots[] = $_h12 . ':' . str_pad((string) $_mm, 2, '0', STR_PAD_LEFT) . ' ' . $_ap;
            }
            $field['options'] = implode("\n", $_slots);
            $fType = 'select'; // hand off to the custom dropdown renderer below
        }

        if ($fType === 'checkbox') {
            // v2.1.1382: checkbox label honours field_label_color override.
            $_chkLblColor = $fieldLabelColor !== '' ? $fieldLabelColor : $autoText;
            echo '<div class="impulse-form-field" style="width:100%;display:flex;align-items:flex-start;gap:10px">';
            echo '<input type="checkbox" name="' . esc_attr($fName) . '" style="width:18px;height:18px;margin-top:2px"' . esc_attr($reqAttr) . '>';
            echo '<label style="font-size:14px;color:' . esc_attr($_chkLblColor) . '">' . esc_html($fLabel) . '</label>';
            echo '</div>';
            continue;
        }

        if ($fType === 'textarea') {
            echo '<div class="impulse-form-field" style="width:100%">';
            echo '<textarea name="' . esc_attr($fName) . '" rows="4" style="' . esc_attr($inputStyle) . ';min-height:120px;resize:vertical" placeholder="' . esc_attr($placeholder) . '"' . esc_attr($reqAttr) . '></textarea>';
            echo '</div>';
            continue;
        }

        // Width style + responsive data attrs.
        $widthStyle = $fWidth < 100 ? 'width:calc(' . $fWidth . '% - ' . intval($fieldGap / 2) . 'px)' : 'width:100%';
        // Per-field MOBILE width (<=768px), exposed as a CSS var that the
        // responsive rule in frontend.css consumes. Defaults to 100% (single
        // column on phones); a field with explicit width_mobile (e.g. 50) stays
        // multi-up. Carried by selects + inputs via $widthStyle; checkboxes and
        // textareas hardcode 100% (the var falls back to 100% for them).
        $mobileWidthStyle = $fWidthMobile < 100 ? 'calc(' . $fWidthMobile . '% - ' . intval($fieldGap / 2) . 'px)' : '100%';
        $widthStyle .= ';--imp-fw-m:' . $mobileWidthStyle;

        if ($fType === 'select') {
            // v2.1.1393: custom dropdown built from divs — native <select> open
            // popup is OS-rendered, no CSS can reach the option-highlight (always
            // system accent / blue on Mac/Windows). A custom widget gives full
            // greyscale control. A hidden <input> mirrors the chosen value so the
            // form payload + required validation behave identically to a select.
            $_fOpts = $field['options'] ?? '';
            $_optList = $_fOpts !== '' ? array_values(array_filter(array_map('trim', explode("\n", (string) $_fOpts)))) : [];
            $_selUid = 'imp-sel-' . intval($elId) . '-' . $idx;
            // v2.1.1395: trigger button uses font-size:14px + line-height:1.4 +
            // the same padding as input fields explicitly (overriding browser
            // default button line-height which was producing extra vertical
            // space). Dropdown options dropped from 10px to 6px vertical
            // padding so the list reads as compact, matching Instinctor's
            // "tight where possible" UI rule.
            echo '<div class="impulse-form-field impulse-form-field-wrap impulse-form-dropdown" data-name="' . esc_attr($fName) . '"' . ($fRequired ? ' data-required="1"' : '') . ' style="' . esc_attr($widthStyle) . ';position:relative" data-width-desktop="' . intval($fWidth) . '" data-width-tablet="' . intval($fWidthTablet) . '" data-width-mobile="' . intval($fWidthMobile) . '">';
            echo '<button type="button" class="impulse-form-dropdown-trigger" id="' . esc_attr($_selUid) . '" aria-haspopup="listbox" aria-expanded="false" style="' . esc_attr($inputStyle) . ';display:flex;align-items:center;justify-content:space-between;text-align:left;cursor:pointer;line-height:1.4;font-family:inherit;font-size:14px">';
            echo '<span class="impulse-form-dropdown-value is-placeholder" data-placeholder="' . esc_attr($placeholder) . '" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">' . esc_html($placeholder) . '</span>';
            echo '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-left:8px;opacity:0.6"><polyline points="6 9 12 15 18 9"/></svg>';
            echo '</button>';
            echo '<div class="impulse-form-dropdown-list" role="listbox" hidden style="' . esc_attr('position:absolute;top:calc(100% + 2px);left:0;right:0;z-index:50;max-height:280px;overflow-y:auto;background:' . $fieldBg . ';border:' . $fieldBorderWidth . 'px solid ' . $fieldBorderColor . ';border-radius:' . $fieldRadius . 'px;box-shadow:0 6px 24px rgba(0,0,0,0.18);padding:2px 0;font-size:14px') . '">';
            foreach ($_optList as $_opt) {
                echo '<div class="impulse-form-dropdown-option" role="option" data-value="' . esc_attr($_opt) . '" style="padding:6px 16px;cursor:pointer;color:' . esc_attr($fieldTextOut) . ';transition:background 0.12s ease-in-out;line-height:1.4">' . esc_html($_opt) . '</div>';
            }
            echo '</div>';
            echo '<input type="hidden" name="' . esc_attr($fName) . '" value=""' . esc_attr($reqAttr) . '>';
            echo '</div>';
            continue;
        }

        // Native HTML input types pass through (date/time/number give real
        // pickers); anything else falls back to text. The field-builder stores
        // 'phone' for tel inputs, so alias it.
        if ($fType === 'phone') $fType = 'tel';
        $_nativeTypes = ['email', 'tel', 'date', 'time', 'number', 'url'];
        $inputType = in_array($fType, $_nativeTypes, true) ? $fType : 'text';
        echo '<div class="impulse-form-field impulse-form-field-wrap" style="' . esc_attr($widthStyle) . '" data-width-desktop="' . intval($fWidth) . '" data-width-tablet="' . intval($fWidthTablet) . '" data-width-mobile="' . intval($fWidthMobile) . '">';
        echo '<input type="' . esc_attr($inputType) . '" name="' . esc_attr($fName) . '" style="' . esc_attr($inputStyle) . '" placeholder="' . esc_attr($placeholder) . '"' . esc_attr($reqAttr);
        foreach (['min', 'max', 'step'] as $_a) {
            if (isset($field[$_a]) && $field[$_a] !== '') echo ' ' . esc_attr($_a) . '="' . esc_attr((string) $field[$_a]) . '"';
        }
        echo '>';
        echo '</div>';
    }

    // Hidden form metadata.
    echo '<input type="hidden" name="form_name" value="' . esc_attr($formName) . '">';
    echo '<input type="hidden" name="form_id"   value="' . esc_attr((string) $elId) . '">';
    // Page id lets the submit handler read this block's own notify_sms setting
    // server-side (per-form SMS opt-in), so the toggle can't be forged. Resolve
    // it the same way impulse-page.php does: the /:slug route does NOT define
    // INSTINCTOR_RENDER_PAGE_ID (the template resolves the slug itself), so we
    // must map REQUEST_URI -> page here, or every non-front page would fall back
    // to the front-page id and the lookup would miss.
    $_twPageId = defined('INSTINCTOR_RENDER_PAGE_ID') ? (int) INSTINCTOR_RENDER_PAGE_ID : 0;
    if (!$_twPageId && !empty($_SERVER['REQUEST_URI']) && class_exists('\Instinctor\Services\PageService')) {
        $_twSlug = trim((string) wp_parse_url(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])), PHP_URL_PATH), '/');
        if ($_twSlug !== '') {
            $_twPg = \Instinctor\Services\PageService::getBySlug($_twSlug);
            if ($_twPg) $_twPageId = (int) ($_twPg['id'] ?? 0);
        }
    }
    if (!$_twPageId) $_twPageId = (int) \Instinctor\Core\Config::get('instinctor_front_page_id', 0);
    echo '<input type="hidden" name="page_id"   value="' . esc_attr((string) $_twPageId) . '">';

    // Submit button.
    $btnWidthStyle = $buttonWidth < 100 ? 'width:' . $buttonWidth . '%' : 'width:100%';
    echo '<div style="width:100%">';
    echo '<button type="submit" class="impulse-form-submit" style="' . esc_attr($btnWidthStyle . ';min-width:fit-content;background:' . $buttonBg . ';color:' . $buttonColor . ';padding:12px 28px;border:none;border-radius:' . $fieldRadius . 'px;font-size:15px;font-weight:600;cursor:pointer;white-space:nowrap') . '">' . esc_html($buttonText) . '</button>';
    echo '</div>';

    echo '</form></div></div>';
    if ($visBadge) echo '</div>';

    // v2.1.1393: custom dropdown wiring. One-time singleton script per page —
    // delegates clicks at document level so it applies to every contact form
    // and survives DOM re-renders. Open trigger → toggle list. Click option →
    // set hidden input value + update visible text. Click outside → close.
    // Keyboard: Enter/Space open, arrow up/down navigate, Esc close, Enter
    // select. The hidden input carries `required` so native form-required
    // validation still triggers if the field is empty on submit.
    if (empty($GLOBALS['impulse_form_dropdown_script_emitted'])) {
        $GLOBALS['impulse_form_dropdown_script_emitted'] = true;
        ob_start();
        echo '(function(){if(window._impFormDropdownInit)return;window._impFormDropdownInit=1;'
           . 'function closeAll(except){document.querySelectorAll(".impulse-form-dropdown-list").forEach(function(l){if(l===except)return;l.hidden=true;var t=l.previousElementSibling;if(t&&t.classList.contains("impulse-form-dropdown-trigger"))t.setAttribute("aria-expanded","false");});}'
           /* v2.1.1394: capture phase. The editor iframe has a body-level
              click handler that calls stopPropagation() before bubble reaches
              document — so the dropdown trigger never received the click. */
           . 'document.addEventListener("click",function(e){'
           . 'var trig=e.target.closest(".impulse-form-dropdown-trigger");'
           . 'if(trig){var wrap=trig.closest(".impulse-form-dropdown");var list=wrap.querySelector(".impulse-form-dropdown-list");var isOpen=!list.hidden;closeAll(list);list.hidden=isOpen;trig.setAttribute("aria-expanded",isOpen?"false":"true");e.preventDefault();e.stopPropagation();return;}'
           . 'var opt=e.target.closest(".impulse-form-dropdown-option");'
           . 'if(opt){var wrap=opt.closest(".impulse-form-dropdown");var val=opt.getAttribute("data-value");var hidden=wrap.querySelector("input[type=hidden]");if(hidden)hidden.value=val;var label=wrap.querySelector(".impulse-form-dropdown-value");if(label){label.textContent=val;label.classList.remove("is-placeholder");}wrap.querySelectorAll(".impulse-form-dropdown-option").forEach(function(o){o.classList.remove("is-active");});opt.classList.add("is-active");var list=wrap.querySelector(".impulse-form-dropdown-list");list.hidden=true;var trig=wrap.querySelector(".impulse-form-dropdown-trigger");if(trig)trig.setAttribute("aria-expanded","false");e.preventDefault();e.stopPropagation();return;}'
           . 'if(!e.target.closest(".impulse-form-dropdown"))closeAll(null);'
           . '},true);'
           . 'document.addEventListener("keydown",function(e){'
           . 'var trig=document.activeElement&&document.activeElement.classList&&document.activeElement.classList.contains("impulse-form-dropdown-trigger")?document.activeElement:null;'
           . 'if(!trig)return;'
           . 'var wrap=trig.closest(".impulse-form-dropdown");var list=wrap.querySelector(".impulse-form-dropdown-list");'
           . 'if(e.key==="Enter"||e.key===" "){var isOpen=!list.hidden;closeAll(list);list.hidden=isOpen;trig.setAttribute("aria-expanded",isOpen?"false":"true");e.preventDefault();}'
           . 'else if(e.key==="Escape"){list.hidden=true;trig.setAttribute("aria-expanded","false");}'
           . '});'
           . '})();';
        $_inlineJs = ob_get_clean();
        wp_add_inline_script('instinctor-frontend-js', $_inlineJs, 'after');
    }

    return ob_get_clean();
}
