/**
 * Glance Live Search - Live AJAX (vanilla JS)
 * Results appear in a dropdown under the bar; on phones the panel takes over.
 */
(function () {
    'use strict';

    /* Why the paid parts of this file are not split out
     * -------------------------------------------------
     * Freemius removes whole files and PHP if-blocks; it does not touch
     * JavaScript. Moving click tracking, suggestions and the price chips into a
     * __premium_only script would mean exposing cfg, wcAjaxUrl, applyTerm,
     * escHtml and announce on a public object so the second file could reach
     * them - 31 references to cfg alone - and that API would then have to be
     * kept stable forever.
     *
     * What it buys is about 1.5 KB minified. The free build ships those
     * branches inert instead: their markup is never rendered and their
     * endpoints do not exist there, so nothing can reach them. Dead weight, not
     * a working paid feature.
     */

    const cfg = window.glseSearch || {};
    const minChars = cfg.min_chars || 3;
    const mobileBreakpoint = cfg.mobile_overlay_breakpoint || 768;
    const isMobile = () => window.matchMedia('(max-width: ' + mobileBreakpoint + 'px)').matches;
    const mobileOverlayEnabled = !!cfg.mobile_overlay && cfg.mobile_overlay !== '0' && cfg.mobile_overlay !== '';

    /*
     * "Disable on mobile" is decided here, from the same media query as the
     * breakpoint setting. Deciding it server-side from the user agent
     * disagrees with that breakpoint, and a cached page then serves one
     * verdict to every visitor alike.
     */
    const disableOnMobile = !!cfg.disable_mobile && cfg.disable_mobile !== '0' && cfg.disable_mobile !== '';

    document.addEventListener('DOMContentLoaded', () => {
        if (disableOnMobile && isMobile()) return;   /* plain form below the breakpoint */

        document.querySelectorAll('.glse-search-wrapper').forEach(wrap => {
            initDropdown(wrap);
            if (mobileOverlayEnabled) {
                initOverlay(wrap);
            }
        });
    });

    /* ─── Search identity ─────────────────────────────────────────────
     * One id per typing session, sent with every search request. The server
     * upserts on it, so "shi" then "shirt" refine a single row whose final
     * term is "shirt", and a click can be attached to the exact search that
     * produced the results the visitor was looking at.
     *
     * Generated in memory only - never stored, never sent anywhere else, so
     * it identifies a search rather than a person.
     */

    var _searchId = null;

    function newSearchId() {
        var bytes = new Uint8Array(16);
        if (window.crypto && window.crypto.getRandomValues) {
            window.crypto.getRandomValues(bytes);
        } else {
            for (var i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
        }
        return Array.from(bytes, function (b) {
            return b.toString(16).padStart(2, '0');
        }).join('');
    }

    /** Current session id, creating one if the session just started. */
    function currentSearchId() {
        if (!_searchId) _searchId = newSearchId();
        return _searchId;
    }

    /** Called when the field empties: the next term starts a new session. */
    function endSearchSession() {
        _searchId = null;
    }


    /* Click delegation on history (items + clear button) */
    document.addEventListener('click', function (e) {
        /* Clear button */
        if (e.target.closest('.glse-history-clear')) {
            e.preventDefault();
            historyClear();
            var container = e.target.closest('.glse-results-container');
            if (container) {
                container.classList.remove('glse-visible');
                container.innerHTML = '';
            }
            return;
        }
        /* History item click → fill input and trigger search */
        var item = e.target.closest('.glse-history-item[data-glse-history]');
        if (!item) return;
        applyTerm(item, item.dataset.glseHistory);
    });


    /* Writes a term into the search box that owns the clicked element.
     *
     * Resolving the input from the item's own wrapper, rather than searching
     * the document, is what makes this correct on a page carrying more than one
     * search bar - a header bar and an in-page block, say. The previous
     * document-wide lookup could fill a different bar than the one clicked.
     *
     * The input event is dispatched instead of calling the search directly so
     * the request keeps the category, price range, caching and announcements
     * that the normal typing path already applies. */
    function applyTerm(item, term) {
        if (!term) return;

        var scope = item.closest('.glse-mobile-overlay') || item.closest('.glse-search-wrapper');
        var searchInput = scope ? scope.querySelector('.glse-search-input') : null;

        if (!searchInput) {
            searchInput = document.querySelector('.glse-search-input:focus')
                || document.querySelector('.glse-mobile-overlay--open .glse-search-input')
                || document.querySelector('.glse-search-input');
        }
        if (!searchInput) return;

        searchInput.value = term;
        /* Focus returns to the input: the shopper is still writing a query, and
         * leaving focus on a row that is about to be replaced would strand
         * keyboard and screen-reader users on a removed element. */
        searchInput.focus();
        searchInput.dispatchEvent(new Event('input'));
    }

    /* ─── Client-side result cache (multi-term) ─── */

    var _cache = new Map();
    var GLSE_MAX_CACHE = 20;
    var GLSE_CACHE_TTL = 5 * 60 * 1000; /* 5 minutes TTL */

    function cacheKey(term, cat) {
        return (cat || '') + '::' + term.toLowerCase();
    }

    function cacheGet(term, cat) {
        var key = cacheKey(term, cat);
        var entry = _cache.get(key);
        if (!entry) return null;
        /* TTL expiration */
        if (Date.now() - entry._cachedAt > GLSE_CACHE_TTL) {
            _cache.delete(key);
            return null;
        }
        return entry;
    }

    function cacheSet(term, cat, json) {
        var key = cacheKey(term, cat);
        if (_cache.size >= GLSE_MAX_CACHE) {
            _cache.delete(_cache.keys().next().value); /* FIFO eviction */
        }
        var entry = Object.assign({}, json);
        entry._cachedAt = Date.now();
        _cache.set(key, entry);
    }

    /* ─── Search history (localStorage) ─── */

    var GLSE_HISTORY_KEY = 'glse_search_history';
    var historyMax = cfg.history_max || 5;
    var historyEnabled = !!cfg.show_history && cfg.show_history !== '0' && cfg.show_history !== '';

    function historyGet() {
        if (!historyEnabled) return [];
        try {
            var raw = localStorage.getItem(GLSE_HISTORY_KEY);
            var parsed = raw ? JSON.parse(raw) : [];
            return Array.isArray(parsed) ? parsed.filter(function (t) { return typeof t === 'string'; }) : [];
        } catch (e) { return []; }
    }

    function historySave(term) {
        if (!historyEnabled || !term) return;
        try {
            var list = historyGet().filter(function (t) { return t.toLowerCase() !== term.toLowerCase(); });
            list.unshift(term);
            if (list.length > historyMax) list = list.slice(0, historyMax);
            localStorage.setItem(GLSE_HISTORY_KEY, JSON.stringify(list));
        } catch (e) { /* quota exceeded - silently ignore */ }
    }

    function historyClear() {
        try { localStorage.removeItem(GLSE_HISTORY_KEY); } catch (e) { }
    }

    function escHtml(str) {
        return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
    }

    function historyBuildHtml() {
        var list = historyGet();
        if (!list.length) return '';
        var i18n = cfg.i18n || {};
        var html = '<div class="glse-history" role="group" aria-label="' + escHtml(i18n.recent || 'Recent searches') + '">';
        html += '<div class="glse-history-header">';
        html += '<span class="glse-history-title">' + escHtml(i18n.recent || 'Recent searches') + '</span>';
        html += '<button type="button" class="glse-history-clear">' + escHtml(i18n.clear_history || 'Clear') + '</button>';
        html += '</div>';
        for (var j = 0; j < list.length; j++) {
            html += '<div class="glse-result-item glse-history-item" role="option" data-glse-history="' + escHtml(list[j]) + '">';
            html += '<svg class="glse-history-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
            html += '<span class="glse-history-term">' + escHtml(list[j]) + '</span>';
            html += '</div>';
        }
        html += '</div>';
        return html;
    }

    /* ─── WC_AJAX URL builder ─── */

    function wcAjaxUrl(endpoint) {
        if (cfg.wc_ajax_url) {
            return cfg.wc_ajax_url.replace('%%endpoint%%', endpoint);
        }
        /* Fallback to admin-ajax.php */
        return cfg.ajax_url;
    }

    /* ─── Nonce refresh ───────────────────────────────────────────────
     * The nonce is baked into the page HTML, so on a full-page-cached site it
     * outlives its 24h validity and every search comes back 403. Rather than
     * surface that as "Search unavailable" forever, we pull a fresh one from an
     * uncached endpoint and replay the request once.
     */

    var _nonceRefresh = null; /* in-flight promise, shared by concurrent callers */

    function refreshNonce() {
        if (_nonceRefresh) return _nonceRefresh;

        var body = new URLSearchParams();
        if (!cfg.wc_ajax_url) body.set('action', 'glse_refresh_nonce');

        _nonceRefresh = fetch(wcAjaxUrl('glse_refresh_nonce'), {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: body.toString(),
        })
            .then(function (r) { return r.ok ? r.json() : null; })
            .then(function (json) {
                if (json && json.success && json.data && json.data.nonce) {
                    cfg.nonce = json.data.nonce;
                    return true;
                }
                return false;
            })
            .catch(function () { return false; })
            .finally(function () {
                /* Allow a later retry, but not a stampede within this cycle. */
                setTimeout(function () { _nonceRefresh = null; }, 1000);
            });

        return _nonceRefresh;
    }

    /** Active price range for a bar, or an empty range when no chip is set. */
    function priceRangeOf(wrap) {
        var chip = wrap && wrap.querySelector('.glse-price-chip--active');
        return chip
            ? { min: chip.dataset.glseMin || '', max: chip.dataset.glseMax || '' }
            : { min: '', max: '' };
    }


    /* ─── Shared fetch helper ─── */

    function doSearch(term, catValue, priceRange, abortRef, onDone) {
        /* Check client cache first */
        /* The range is part of the cache identity: the same term under two
           different price bands is two different result sets. */
        var cacheTag = (catValue || '') + '|' + (priceRange.min || '') + '-' + (priceRange.max || '');
        var cached = cacheGet(term, cacheTag);
        if (cached) {
            cached._fromCache = true;
            onDone(cached);
            return;
        }

        if (abortRef.ctrl) abortRef.ctrl.abort();
        abortRef.ctrl = new AbortController();

        /* Auto-abort after 10 s to avoid infinite spinner */
        var timeoutId = setTimeout(function () { abortRef.ctrl.abort(); }, 10000);

        var url = wcAjaxUrl('glse_live_search');
        var body = new URLSearchParams();
        /* action only needed for admin-ajax fallback */
        if (!cfg.wc_ajax_url) body.set('action', 'glse_live_search');
        body.set('nonce', cfg.nonce);
        body.set('term', term);
        body.set('search_id', currentSearchId());
        if (catValue) body.set('cat', catValue);
        if (priceRange.min) body.set('min_price', priceRange.min);
        if (priceRange.max) body.set('max_price', priceRange.max);
        if (cfg.lang) body.set('lang', cfg.lang);

        var t0 = performance.now();

        function send(isRetry) {
            body.set('nonce', cfg.nonce);
            return fetch(url, {
                method: 'POST',
                signal: abortRef.ctrl.signal,
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                body: body.toString(),
            })
                .then(function (r) {
                    /* Stale nonce (cached page): refresh once, then replay. */
                    if (r.status === 403 && !isRetry) {
                        return refreshNonce().then(function (ok) {
                            if (!ok) throw new Error('HTTP 403');
                            return send(true);
                        });
                    }
                    if (!r.ok) throw new Error('HTTP ' + r.status);
                    return r.json();
                });
        }

        send(false)
            .then(function (json) {
                clearTimeout(timeoutId);
                json._responseMs = Math.round(performance.now() - t0);
                if (json && json.success) {
                    cacheSet(term, cacheTag, json);
                }
                onDone(json);
            })
            .catch(function (err) {
                clearTimeout(timeoutId);
                if (err.name === 'AbortError') return;
                onDone({ success: false, _networkError: true });
            });
    }

    /* ─── Shared focus trap (used by mobile overlay) ─── */

    var _trapHandler = null;

    function getFocusable(el) {
        return Array.from(el.querySelectorAll(
            'a[href], button:not([disabled]), input:not([disabled]), ' +
            'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
        )).filter(function (node) {
            var rect = node.getBoundingClientRect();
            if (rect.width === 0 && rect.height === 0) return false;
            var style = window.getComputedStyle(node);
            return style.visibility !== 'hidden' && style.display !== 'none';
        });
    }

    function trapFocus(el) {
        releaseFocusTrap();
        _trapHandler = function (e) {
            if (e.key !== 'Tab') return;
            var focusable = getFocusable(el);
            if (!focusable.length) return;
            var first = focusable[0], last = focusable[focusable.length - 1];
            if (e.shiftKey) {
                if (document.activeElement === first) { e.preventDefault(); last.focus(); }
            } else {
                if (document.activeElement === last) { e.preventDefault(); first.focus(); }
            }
        };
        document.addEventListener('keydown', _trapHandler);
    }

    function releaseFocusTrap() {
        if (_trapHandler) { document.removeEventListener('keydown', _trapHandler); _trapHandler = null; }
    }

    /* ─── Shared result-container helpers factory ─── */

    /**
     * Announces a short summary to assistive tech.
     *
     * The listbox itself carries no aria-live: a live listbox makes screen
     * readers re-read its whole contents on every keystroke, on top of the
     * active option.
     */
    function announce(wrapper, message) {
        var node = wrapper && wrapper.querySelector('.glse-sr-status');
        if (node) node.textContent = message;
    }

    /* Builds the sentence read out after a search.
     *
     * The correction and the history rows are .glse-result-item too - they have
     * to be, so the arrow keys reach them - but counting them as results would
     * announce a number nobody searched for. They are named for what they are
     * instead. */
    function summarise(container) {
        var i18n = cfg.i18n || {};
        var products = container.querySelectorAll(
            '.glse-result-item:not(.glse-correction-item):not(.glse-history-item)'
        ).length;
        var history = container.querySelectorAll('.glse-history-item').length;
        var correction = container.querySelector('.glse-correction-item');

        if (correction) {
            var term = correction.dataset.glseSuggest || '';
            return i18n.correction_offered
                ? i18n.correction_offered.replace('%s', term)
                : 'No results. Did you mean ' + term + '?';
        }

        /* The panel opened on focus shows past searches, not answers to a
           query. Calling them "results" told screen-reader users a search had
           run when none had. */
        if (history && !products) {
            return i18n.history_count
                ? i18n.history_count.replace('%d', history)
                : history + ' recent searches';
        }

        var parts = [];
        parts.push(i18n.results_count
            ? i18n.results_count.replace('%d', products)
            : products + ' results');

        return parts.join(', ');
    }

    function makeResultHelpers(container, comboboxInput) {
        var activeIdx = -1;
        var wrapper = container.closest('.glse-search-wrapper');
        var idPrefix = (container.id || 'glse') + '-result-';
        var getItems = function () { return container.querySelectorAll('.glse-result-item'); };

        function setExpanded(open) {
            if (comboboxInput) comboboxInput.setAttribute('aria-expanded', open ? 'true' : 'false');
        }

        function showResults(html) {
            container.setAttribute('aria-busy', 'false');
            container.innerHTML = html;
            // Add role="option" and aria-selected to each result item
            container.querySelectorAll('.glse-result-item').forEach(function (el, i) {
                el.setAttribute('role', 'option');
                el.setAttribute('aria-selected', 'false');
                if (!el.id) el.id = idPrefix + i;
            });
            container.classList.add('glse-visible');
            setExpanded(true);
            if (wrapper) wrapper.classList.add('glse-has-results');
            announce(wrapper, summarise(container));
            activeIdx = -1;
        }

        function hideResults() {
            announce(wrapper, '');
            container.classList.remove('glse-visible');
            setExpanded(false);
            if (comboboxInput) comboboxInput.removeAttribute('aria-activedescendant');
            if (wrapper) wrapper.classList.remove('glse-has-results');
            // Clear content after CSS transition ends (120ms) to allow fade-out.
            setTimeout(function () {
                if (!container.classList.contains('glse-visible')) {
                    container.innerHTML = '';
                }
            }, 150);
            activeIdx = -1;
        }

        function showHint(msg) {
            container.setAttribute('aria-busy', 'false');
            var hint = document.createElement('div');
            hint.className = 'glse-hint-message';
            hint.textContent = msg || '';
            container.innerHTML = '';
            container.appendChild(hint);
            container.classList.add('glse-visible');
            setExpanded(true);
        }

        function setActive(idx) {
            var items = getItems();
            if (!items.length) return;
            items.forEach(function (el) {
                el.classList.remove('glse-active');
                el.setAttribute('aria-selected', 'false');
            });
            activeIdx = (idx + items.length) % items.length;
            items[activeIdx].classList.add('glse-active');
            items[activeIdx].setAttribute('aria-selected', 'true');
            items[activeIdx].scrollIntoView({ block: 'nearest' });
            if (comboboxInput) comboboxInput.setAttribute('aria-activedescendant', items[activeIdx].id);
        }

        return {
            getItems: getItems, showResults: showResults, hideResults: hideResults,
            showHint: showHint, setActive: setActive, getActiveIdx: function () { return activeIdx; }
        };
    }

    /* ══════════════════════════════════════════════
       MODE DROPDOWN
       ══════════════════════════════════════════════ */

    function initDropdown(wrap) {
        var form = wrap.querySelector('.glse-search-form');
        var input = wrap.querySelector('.glse-search-input');
        var catSelect = wrap.querySelector('.glse-cat-select');
        var results = wrap.querySelector('.glse-results-container');
        var spinner = wrap.querySelector('.glse-search-spinner');
        if (!input || !results) return;


        var timer = null;
        var abortRef = { ctrl: null };

        var h = makeResultHelpers(results, input);
        var getItems = h.getItems, showResults = h.showResults, hideResults = h.hideResults;
        var showHint = h.showHint, setActive = h.setActive, getActiveIdx = h.getActiveIdx;

        /* keyboard */
        input.addEventListener('keydown', function (e) {
            var items = getItems();
            var open  = results.classList.contains('glse-visible');

            /* Arrow down on a closed but filled field reopens the list, as the
               combobox pattern expects. */
            if (!open && 'ArrowDown' === e.key && input.value.trim().length >= minChars) {
                e.preventDefault();
                input.dispatchEvent(new Event('input'));
                return;
            }

            if (!items.length || !open) return;
            switch (e.key) {
                case 'ArrowDown': e.preventDefault(); setActive(getActiveIdx() + 1); break;
                case 'ArrowUp': e.preventDefault(); setActive(getActiveIdx() === -1 ? -1 : getActiveIdx() - 1); break;
                case 'Home': e.preventDefault(); setActive(0); break;
                case 'End': e.preventDefault(); setActive(items.length - 1); break;
                case 'Enter':
                    if (getActiveIdx() >= 0 && items[getActiveIdx()]) {
                        e.preventDefault();
                        items[getActiveIdx()].click();
                    }
                    break;
                /* Focus stays in the field: blurring it forced a tab or a click to search again. */
                case 'Escape': e.preventDefault(); hideResults(); break;
                case 'Tab': hideResults(); break;
            }
        });

        /* form submit */
        if (form) {
            form.addEventListener('submit', function (e) {
                if (results.classList.contains('glse-visible') && getActiveIdx() >= 0) {
                    e.preventDefault();
                }
            });
        }

        /* live search */
        var GLSE_MAX_TERM = 200;
        input.addEventListener('input', function () {
            clearTimeout(timer);
            if (spinner) spinner.classList.remove('active');
            var term = input.value.trim().substring(0, GLSE_MAX_TERM);

            // Clear immediately only when field is empty or below minimum.
            if (!term) { endSearchSession(); hideResults(); return; }

            if (term.length < minChars) {
                hideResults();
                showHint(cfg.hint_message);
                return;
            }

            // Keep existing results visible while fetching new ones (no flash).
            var catVal = catSelect ? catSelect.value : '';
            /* Show results instantly from cache (no spinner, no analytics) */
            var cached = cacheGet(term, catVal);
            if (cached && cached.success) {
                showResults(cached.data.html);
                historySave(term);
                return;
            }
            timer = setTimeout(function () {
                if (spinner) spinner.classList.add('active');
                results.setAttribute('aria-busy', 'true');
                doSearch(term, catVal, priceRangeOf(wrap), abortRef, function (json) {
                    if (spinner) spinner.classList.remove('active');
                    if (json && json._networkError) { showHint(cfg.error_message || 'Search unavailable. Please try again.'); results.querySelector('.glse-hint-message').classList.add('glse-hint-message--error'); return; }
                    if (json && json.success && json.data) {
                        showResults(json.data.html);
                        historySave(term);
                    } else {
                        showHint(cfg.error_message || 'Search unavailable. Please try again.');
                    }
                });
            }, cfg.delay != null ? cfg.delay : 300);
        });

        /* re-focus : historique si vide, cache si possible, sinon relancer */
        input.addEventListener('focus', function () {
            /* Skip if mobile overlay will handle this focus event */
            if (mobileOverlayEnabled && isMobile()) return;
            if (results.classList.contains('glse-visible')) return;
            var term = input.value.trim();
            if (term.length >= minChars) {
                var catVal = catSelect ? catSelect.value : '';
                var cached = cacheGet(term, catVal);
                if (cached && cached.success) {
                    showResults(cached.data.html);
                } else {
                    input.dispatchEvent(new Event('input'));
                }
            } else if (!term && historyEnabled) {
                var hHtml = historyBuildHtml();
                if (hHtml) showResults(hHtml);
            }
        });

        /* category change */
        if (catSelect) {
            catSelect.addEventListener('change', function () {
                if (input.value.trim().length >= minChars) {
                    input.dispatchEvent(new Event('input'));
                }
            });
        }

        /* close on click outside */
                document.body.appendChild(results);
        results.style.position = 'absolute';
        results.style.zIndex = '999999';
        results.style.display = '';

        var computed = getComputedStyle(wrap);
        var glseVars = ['--glse-primary', '--glse-primary-rgb', '--glse-border-width',
            '--glse-border-color', '--glse-radius', '--glse-font-size', '--glse-text-color',
            '--glse-placeholder-color', '--glse-shadow', '--glse-btn-bg', '--glse-btn-color',
            '--glse-bar-height'];
        glseVars.forEach(function (v) {
            var val = computed.getPropertyValue(v);
            if (val) results.style.setProperty(v, val);
        });

        document.addEventListener('click', function (e) {
            if (!wrap.contains(e.target) && !results.contains(e.target)) hideResults();
        });
    

        /* mobile: tap wrapper → focus input */
        if (isMobile()) {
            wrap.addEventListener('click', function (e) {
                if (e.target !== input && !e.target.closest('.glse-search-btn')) {
                    input.focus();
                }
            });
        }


                var GAP = 2; /* px between bar bottom border and dropdown */

        /*
         * Position the dropdown
         */
        function syncDropdownPosition() {
            var rect    = wrap.getBoundingClientRect();
            var bodyPos = getComputedStyle(document.body).position;
            var top, left;

            if (bodyPos === 'relative' || bodyPos === 'absolute' || bodyPos === 'fixed') {
                var bodyRect = document.body.getBoundingClientRect();
                top  = rect.bottom + window.scrollY - (bodyRect.top + window.scrollY) + GAP;
                left = rect.left + window.scrollX - (bodyRect.left + window.scrollX);
            } else {
                top  = rect.bottom + window.scrollY + GAP;
                left = rect.left + window.scrollX;
            }

            results.style.top   = top + 'px';
            results.style.left  = left + 'px';
            results.style.width = rect.width + 'px';
        }

        /* Throttled sync: only fires on scroll/resize via rAF coalescing */
        var _rafPending = false;
        function onScrollOrResize() {
            if (_rafPending) return;
            _rafPending = true;
            requestAnimationFrame(function () {
                _rafPending = false;
                if (results.classList.contains('glse-visible')) syncDropdownPosition();
            });
        }

        function startTracking() {
            window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true });
            window.addEventListener('resize', onScrollOrResize, { passive: true });
        }

        function stopTracking() {
            window.removeEventListener('scroll', onScrollOrResize, { capture: true });
            window.removeEventListener('resize', onScrollOrResize);
        }

        var _origShowResults = showResults;
        showResults = function (html) {
            syncDropdownPosition();
            _origShowResults(html);
            startTracking();
        };

        var _origHideResults = hideResults;
        hideResults = function () {
            stopTracking();
            _origHideResults();
        };

        var _origShowHint = showHint;
        showHint = function (msg) {
            syncDropdownPosition();
            _origShowHint(msg);
            startTracking();
        };
    
    }

    /* ══════════════════════════════════════════════
       MOBILE OVERLAY (auto-triggered on small screens)
       ══════════════════════════════════════════════ */

    /**
     * Fullscreen search panel.
     *
     * @param {Element} wrap
     */
    function initOverlay(wrap) {
        var origInput = wrap.querySelector('.glse-search-input');
        var origForm = wrap.querySelector('.glse-search-form');
        var origCatSelect = wrap.querySelector('.glse-cat-select');
        if (!origInput) return;

        var overlay = null;
        var moInput = null;
        var moCatSelect = null;
        var moResults = null;
        var moSpinner = null;
        var timer = null;
        var abortRef = { ctrl: null };
        var getItems, showResults, hideResults, showHint, setActive, getActiveIdx;

        function buildOverlay() {
            if (overlay) return;

            overlay = document.createElement('div');
            overlay.className = 'glse-mobile-overlay';
            overlay.style.zIndex = '2147483647';
            overlay.setAttribute('role', 'dialog');
            overlay.setAttribute('aria-modal', 'true');

            var panel = document.createElement('div');
            panel.className = 'glse-mobile-overlay-panel glse-search-wrapper';
            var origStyle = wrap.getAttribute('style');
            if (origStyle) {
                var cssVars = origStyle.match(/--glse-[^:]+:[^;]+/g);
                if (cssVars) panel.setAttribute('style', cssVars.join(';'));
            }

            /* header: input + close */
            var header = document.createElement('div');
            header.className = 'glse-mobile-overlay-header';

            var form = document.createElement('form');
            form.id = 'glse-mo-form-' + Date.now();
            form.className = 'glse-search-form glse-mobile-overlay-form';
            form.action = origForm ? origForm.action : (window.location.origin + '/');
            form.method = 'get';
            var hid = document.createElement('input');
            hid.type = 'hidden'; hid.name = 'post_type'; hid.value = 'product';
            form.appendChild(hid);

            /* Category select clone, shown full width below the header.
             *
             * It sits outside the form on purpose - the layout puts it under
             * the header, not inside it - so it is tied back with the form
             * attribute. Without that the live search filters correctly while
             * typing, then Enter submits without product_cat and the results
             * page ignores the category the shopper picked. Same failure the
             * desktop bar had, reached a different way. */
            if (origCatSelect && origCatSelect.tagName === 'SELECT') {
                moCatSelect = origCatSelect.cloneNode(true);
                moCatSelect.classList.add('glse-mobile-overlay-cat');
                moCatSelect.setAttribute('form', form.id);
                moCatSelect.addEventListener('change', function () {
                    if (moInput.value.trim().length >= minChars) {
                        moInput.dispatchEvent(new Event('input'));
                    }
                });
            }

            var ig = document.createElement('div');
            ig.className = 'glse-input-group';

            moInput = document.createElement('input');
            moInput.type = 'text';
            moInput.className = 'glse-search-input';
            moInput.name = 's';
            moInput.placeholder = origInput.placeholder;
            moInput.setAttribute('autocomplete', 'off');
            moInput.setAttribute('inputmode', 'search');
            moInput.setAttribute('role', 'combobox');
            moInput.setAttribute('aria-autocomplete', 'list');
            moInput.setAttribute('aria-haspopup', 'listbox');
            moInput.setAttribute('aria-expanded', 'false');
            moInput.setAttribute('aria-label', origInput.getAttribute('aria-label') || '');

            moSpinner = document.createElement('div');
            moSpinner.className = 'glse-search-spinner';
            moSpinner.setAttribute('aria-hidden', 'true');

            ig.appendChild(moInput);
            ig.appendChild(moSpinner);
            form.appendChild(ig);

            var closeBtn = document.createElement('button');
            closeBtn.type = 'button';
            closeBtn.className = 'glse-mobile-overlay-close';
            closeBtn.setAttribute('aria-label', (cfg.i18n && cfg.i18n.close) || 'Close');
            closeBtn.textContent = '\u2715'; /* × */
            closeBtn.addEventListener('click', closeMobileOverlay);

            header.appendChild(form);
            header.appendChild(closeBtn);

            moResults = document.createElement('div');
            moResults.id = 'glse-mo-results-' + Date.now();
            moResults.className = 'glse-results-container glse-mobile-overlay-results';
            moResults.setAttribute('role', 'listbox');
            moInput.setAttribute('aria-controls', moResults.id);

            panel.appendChild(header);
            if (moCatSelect) panel.appendChild(moCatSelect);
            panel.appendChild(moResults);
            overlay.appendChild(panel);
            document.body.appendChild(overlay);

            var h = makeResultHelpers(moResults, moInput);
            getItems = h.getItems;
            showResults = h.showResults;
            hideResults = h.hideResults;
            showHint = h.showHint;
            setActive = h.setActive;
            getActiveIdx = h.getActiveIdx;

            /* live search */
            moInput.addEventListener('input', function () {
                clearTimeout(timer);
                moSpinner.classList.remove('active');
                var term = moInput.value.trim().substring(0, 200);
                if (!term) { endSearchSession(); hideResults(); return; }
                if (term.length < minChars) { hideResults(); showHint(cfg.hint_message); return; }
                var catVal = moCatSelect ? moCatSelect.value : '';
                var cached = cacheGet(term, catVal);
                if (cached && cached.success) {
                    showResults(cached.data.html);
                    historySave(term);
                    return;
                }
                timer = setTimeout(function () {
                    moSpinner.classList.add('active');
                    moResults.setAttribute('aria-busy', 'true');
                    doSearch(term, catVal, priceRangeOf(wrap), abortRef, function (json) {
                        moSpinner.classList.remove('active');
                        if (json && json._networkError) { showHint(cfg.error_message || 'Search unavailable. Please try again.'); moResults.querySelector('.glse-hint-message').classList.add('glse-hint-message--error'); return; }
                        if (json && json.success) {
                            showResults(json.data.html);
                            historySave(term);
                        } else {
                            showHint(cfg.error_message || 'Search unavailable. Please try again.');
                        }
                    });
                }, cfg.delay != null ? cfg.delay : 300);
            });

            /* keyboard */
            moInput.addEventListener('keydown', function (e) {
                var items = getItems();
                switch (e.key) {
                    case 'ArrowDown': if (items.length) { e.preventDefault(); setActive(getActiveIdx() + 1); } break;
                    case 'ArrowUp': if (items.length) { e.preventDefault(); setActive(getActiveIdx() - 1); } break;
                    case 'Home': if (items.length) { e.preventDefault(); setActive(0); } break;
                    case 'End': if (items.length) { e.preventDefault(); setActive(items.length - 1); } break;
                    case 'Enter': if (getActiveIdx() >= 0 && items[getActiveIdx()]) { e.preventDefault(); items[getActiveIdx()].click(); } break;
                    case 'Escape': closeMobileOverlay(); break;
                }
            });
        }

        function openMobileOverlay() {
            if (overlay && overlay.classList.contains('glse-mobile-overlay--open')) return;
            buildOverlay();
            overlay.classList.add('glse-mobile-overlay--open');
            document.body.classList.add('glse-body--overlay-open');
            trapFocus(overlay);
            requestAnimationFrame(function () {
                moInput.value = origInput.value;
                moInput.focus();
                if (moInput.value.trim().length >= minChars) {
                    moInput.dispatchEvent(new Event('input'));
                } else if (!moInput.value.trim() && historyEnabled) {
                    var hHtml = historyBuildHtml();
                    if (hHtml) showResults(hHtml);
                }
            });
        }

        var _closingOverlay = false;
        function closeMobileOverlay() {
            if (!overlay || !overlay.classList.contains('glse-mobile-overlay--open')) return;
            _closingOverlay = true;
            origInput.value = moInput.value;
            overlay.classList.remove('glse-mobile-overlay--open');
            document.body.classList.remove('glse-body--overlay-open');
            releaseFocusTrap();
            clearTimeout(timer);
            if (abortRef.ctrl) abortRef.ctrl.abort();
            /* Do NOT focus origInput - it would re-trigger openMobileOverlay */
            setTimeout(function () { _closingOverlay = false; }, 100);
        }

        /* Intercept focus on mobile → open overlay instead of dropdown */
        origInput.addEventListener('focus', function () {
            if (isMobile() && !_closingOverlay) {
                origInput.blur();
                openMobileOverlay();
            }
        });

        /* Close overlay if viewport grows beyond breakpoint (orientation change / resize) */
        /* Growing past the breakpoint returns to the bar. */
        window.matchMedia('(max-width: ' + mobileBreakpoint + 'px)').addEventListener('change', function (e) {
            if (!e.matches) closeMobileOverlay();
        });
    }


    /* ─── Surface for the paid script ─────────────────────────────────
     * search-pro.js is enqueued after this file and picks these up. Four
     * entries, chosen because its handlers cannot get them from the DOM: the
     * config, the character threshold, the AJAX URL builder, and the routine
     * that writes a term into the search box a click came from.
     *
     * Anything added here becomes a contract this file has to keep, so the list
     * stays closed unless a paid handler genuinely cannot work without it.
     */
    window.glseCore = {
        cfg:       cfg,
        minChars:  minChars,
        wcAjaxUrl: wcAjaxUrl,
        applyTerm: applyTerm
    };

})();
