PageSourceSearch

https://kavsanspizza.com/src/js/modules/location_picker.js?v=1790256818

js kavsanspizza.com collected 2026-09-24 14:46:12 UTC 135,955 bytes, 2,381 lines download raw bytes

1'use strict';
2
3/**
4 * Location picker — 7-step modal mirrored from CI3 wla_home_20260507.js.
5 * All API calls go to AppConfig.apiBase (https://api.uengage.in).
6 * Loaded on every page from base.php; auto-opens when AppConfig.autoOpenLocationModal=1
7 * AND localStorage has no userLat/userLng. Header pill is wired site-wide.
8 *
9 * Globals exposed (matching CI3 names referenced from modal markup):
10 *   closeLocationFlow, CurrentAddress, showLocationStep,
11 *   enterAddressSearchLocality, homeSearchLocality,
12 *   openHomeAddressForm, homeAddrGoToCurrentLocation,
13 *   confirmLocationAndShowForm, saveHomeAddress,
14 *   selectOrderTypeAndRedirect, openLoginOrSavedAddresses, changeUserLocation
15 */
16(function () {
17    var AppConfig = window.AppConfig || {};
18    var API_BASE  = (AppConfig.apiBase || '').replace(/\/$/, '');
19    var API_KEY   = AppConfig.staticApiKey || '';
20    var pId       = Number(AppConfig.businessId || 0);
21    var mapsKey   = AppConfig.mapsKey || '';
22    var mapsFlow  = Number(AppConfig.mapsFlow || 0);
23    var useGoogle = Boolean(mapsKey) && mapsFlow === 1;
24
25    function apiHeaders(extra) {
26        var h = { 'api-key': API_KEY };
27        if (extra) Object.keys(extra).forEach(function (k) { h[k] = extra[k]; });
28        return h;
29    }
30
31    // ─── Reset & expiry ─────────────────────────────────────
32    // Every localStorage key the location flow writes — used by logout cleanup and TTL expiry.
33    var _LP_LOCATION_KEYS = [
34        'userLat', 'userLng', 'userAddress', 'userAddressLabel', 'userLocSource',
35        'slug', 'business', 'locality', 'city', 'orderType',
36        'deliveryAddressId', 'selectedDeliveryAddressId', 'selectedAddressId',
37        'formatted_Address', 'locationSelectedAt',
38        // Pre-booking: outlet name cache + chosen slot — clear on logout/TTL too.
39        'pbkOutletName', 'pbkOutletId',
40        'slot_id', 'slot_date', 'slot_label', 'slot_startH', 'slot_endH'
41    ];
42    var _LP_TTL_MS = 60 * 60 * 1000; // 60 minutes — re-prompt for location after this
43
44    // Public reset hook — logout handlers (profile_sidebar.js, userProfile.js) call this.
45    window.resetLocationJourney = function () {
46        _LP_LOCATION_KEYS.forEach(function (k) { localStorage.removeItem(k); });
47        // Also clear per-user cart/order/address caches that must not survive logout:
48        // packs (pkgCartMeta/pkgOrderId), subscriptions (subCartMeta/subOrderId), and the
49        // checkout-address cache co_addr_{pId}_{contactId} (dynamic key → prefix scan).
50        ['pkgCartMeta', 'pkgOrderId', 'subCartMeta', 'subOrderId'].forEach(function (k) { localStorage.removeItem(k); });
51        try {
52            Object.keys(localStorage).forEach(function (k) { if (k.indexOf('co_addr_') === 0) localStorage.removeItem(k); });
53        } catch (e) { /* ignore */ }
54        // Clear the view_business cookie too — it gates which outlet's menu/cart loads.
55        document.cookie = 'view_business=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
56    };
57
58    // TTL: if the user confirmed an outlet more than 60 mins ago, wipe the journey so the
59    // modal re-prompts on the next page. Runs once per page load before maybeAutoOpen().
60    (function _checkLocationTtl() {
61        var ts = parseInt(localStorage.getItem('locationSelectedAt') || '0', 10);
62        if (ts && (Date.now() - ts) > _LP_TTL_MS) window.resetLocationJourney();
63    })();
64
65    // ─── Location flow disabled (location_popup_config = 0) ──────────────────
66    // Replaces CI3 wla_home_20260608.js isSingleOutlet block.
67    // Single outlet → seed slug + navigate directly to /order/{slug}.
68    // Multi outlet  → send to /online-order (store locator) to pick first.
69    if (!Number(AppConfig.locationPopupConfig)) {
70        var isSingle = Number(AppConfig.storesCount) === 1;
71
72        if (isSingle) {
73            // Seed the outlet from home page storesDetails into localStorage.
74            // OVERWRITE unconditionally: with one outlet there is nothing for the
75            // user to choose, so a previously stored slug is a stale cache, not a
76            // selection to preserve. Leaving it in place strands the visitor —
77            // /order/{unknown-slug} 302s back to home, which reads as "the category
78            // click just reloads the page". Renamed outlets and slugs carried over
79            // from an earlier config both land here. city/locality are evicted
80            // before the conditional writes so an outlet whose city/locality is
81            // blank clears the stale values rather than leaving them sitting next
82            // to a freshly written slug.
83            var _singleOutletSlug = (function _seedDisabledOutlet() {
84                var d = window.storesDetails;
85                if (!d || !d.length) return '';
86                var store = d[0];
87                var slug  = store.web_slug || store.slug || '';
88                if (!slug) return '';
89                localStorage.setItem('slug', slug);
90                localStorage.removeItem('city');
91                localStorage.removeItem('locality');
92                if (store.city)     localStorage.setItem('city',     store.city);
93                if (store.locality) localStorage.setItem('locality', store.locality);
94                return slug;
95            })();
96
97            // Server-rendered slug wins over localStorage for the same reason.
98            // Both handlers take the outlet slug as their last argument so neither
99            // depends on window.storesDetails being present on the page.
100            window.silverCatClick = function (sectionSlug, outletSlug) {
101                var slug = outletSlug || _singleOutletSlug || localStorage.getItem('slug') || '';
102                if (!slug || slug === 'null') return;
103                var url = '/order/' + slug;
104                if (sectionSlug) url += '?section_slug=' + encodeURIComponent(sectionSlug);
105                window.location.href = url;
106            };
107            window.silverItemClick = function (sectionSlug, itemSlug, storeCount, outletSlug) {
108                var slug = outletSlug || _singleOutletSlug || localStorage.getItem('slug') || '';
109                if (!slug || slug === 'null') return;
110                var url  = '/order/' + slug;
111                var qs   = [];
112                if (sectionSlug) qs.push('section_slug=' + encodeURIComponent(sectionSlug));
113                if (itemSlug)    qs.push('item_slug='    + encodeURIComponent(itemSlug));
114                if (qs.length)   url += '?' + qs.join('&');
115                window.location.href = url;
116            };
117            // The header's "Deliver to" chip calls changeUserLocation(), which is
118            // only assigned near the bottom of this module — past the return
119            // below. A single-outlet brand therefore had an undefined global and
120            // the chip did nothing at all when clicked. There is no journey to
121            // open here (no #locationFlowModal is rendered either), and the
122            // established behaviour for one outlet is to go straight to its menu,
123            // which is exactly what silverCatClick does above.
124            window.changeUserLocation = function () {
125                var slug = _singleOutletSlug || localStorage.getItem('slug') || '';
126                if (!slug || slug === 'null') return;
127                window.location.href = '/order/' + slug;
128            };
129
130            // The chip reads localStorage on its own (deferred) script run, which
131            // can happen before the seeding above. Nudge it so it shows the outlet
132            // rather than sitting on "Set location" for a brand where a location
133            // cannot be set. No-op when the v2 header is not the one rendered.
134            if (typeof window.hdr2RefreshHeader === 'function') window.hdr2RefreshHeader();
135        } else {
136            // Multi-outlet with flow disabled: send to store locator to pick an outlet.
137            window.silverCatClick  = function () { window.location.href = '/online-order'; };
138            window.silverItemClick = function () { window.location.href = '/online-order'; };
139            window.changeUserLocation = function () { window.location.href = '/online-order'; };
140        }
141        return; // no modal, no info bar — nothing else to initialise
142    }
143
144    var modalEl = document.getElementById('locationFlowModal');
145    if (!modalEl) return;
146
147    var state = {
148        lat: null, lng: null,
149        address: '', locality: '', city: '', stateName: '', pinCode: '',
150        nearest: [], currentStep: 'location', pendingAddr: null,
151        // Promise for an in-flight add/update-address POST. selectOrderTypeAndRedirect awaits
152        // it (bounded) so the freshly-created deliveryAddressId is committed before redirect.
153        savePromise: null
154    };
155    window._lpPrevStep = 'location';
156
157    function userData() {
158        try { return JSON.parse(localStorage.getItem('userdata') || '{}') || {}; } catch (e) { return {}; }
159    }
160
161    // Write the canonical delivery-address selection the checkout page reads
162    // (checkout.js: co_addr_{parentId}_{contactId} → {id,text,name}). pId here is
163    // the parent id, the same brand scope checkout uses, so the address chosen in
164    // this flow is restored on checkout without asking the user to pick again.
165    function persistCheckoutAddress(id, text, name) {
166        var ud = userData();
167        if (!ud.contactId || !id || String(id) === '0') return;
168        try {
169            localStorage.setItem('co_addr_' + pId + '_' + ud.contactId,
170                JSON.stringify({ id: String(id), text: text || '', name: name || '' }));
171        } catch (e) {}
172    }
173
174    // ─── Deferred-address buffer ────────────────────────────
175    // While the user is mid-flow — they've picked/added/searched an address but have NOT
176    // yet been redirected to a real outlet WITH an order type — the selection is held in
177    // memory only. Nothing touches the keys checkout reads (co_addr_*, deliveryAddressId,
178    // userAddress). flushPendingAddr() commits it from selectOrderTypeAndRedirect; an
179    // abandoned flow ("not serviceable" → Explore Menu, or just closing the modal) leaves
180    // no stray address on the checkout page.
181    function setPendingAddr(a)   { state.pendingAddr = a; }
182    function clearPendingAddr()  { state.pendingAddr = null; }
183    function flushPendingAddr() {
184        var a = state.pendingAddr;
185        if (!a) return;
186        if (a.text)     localStorage.setItem('userAddress', a.text);
187        // An EXPLICIT empty label clears the key instead of being skipped. These guards
188        // can only ever set a value, never clear one, so a caller that deliberately has
189        // no address type — the GPS path passes label: '' and says so in a comment —
190        // silently inherited the previous selection's label. `undefined` still means
191        // "this caller isn't touching the label"; '' means "there is no label".
192        if (a.label)                  localStorage.setItem('userAddressLabel', a.label);
193        else if (a.label === '')      localStorage.removeItem('userAddressLabel');
194        if (a.locality) localStorage.setItem('locality', a.locality);
195        if (a.city)     localStorage.setItem('city', a.city);
196        if (a.id && String(a.id) !== '0') {
197            localStorage.setItem('deliveryAddressId', String(a.id));
198            localStorage.setItem('selectedDeliveryAddressId', String(a.id));
199            persistCheckoutAddress(a.id, a.text || '', a.label || 'Home');
200        } else {
201            // A new location was chosen but no saved-address id resolved (add-address upstream
202            // returned no id, or the user picked a raw locality). The previously stored
203            // deliveryAddressId points at a DIFFERENT, older address — clear it so checkout
204            // can't silently reuse the wrong one for this freshly chosen location.
205            var ud = userData();
206            localStorage.removeItem('deliveryAddressId');
207            localStorage.removeItem('selectedDeliveryAddressId');
208            if (ud.contactId) localStorage.removeItem('co_addr_' + pId + '_' + ud.contactId);
209        }
210        state.pendingAddr = null;
211    }
212    function $(sel) { return modalEl.querySelector(sel); }
213    function $$(sel) { return modalEl.querySelectorAll(sel); }
214
215    // ─── Modal show/hide via Bootstrap ──────────────────────
216    var bsModal = null;
217    function getModal() {
218        if (bsModal) return bsModal;
219        if (window.bootstrap && bootstrap.Modal) {
220            bsModal = bootstrap.Modal.getOrCreateInstance(modalEl, { backdrop: 'static', keyboard: true });
221        }
222        return bsModal;
223    }
224    function showOverlay() {
225        // Forced mode (window._lpForceSelect): the caller (e.g. pre-booking detail)
226        // requires a location before proceeding — hide the close buttons so the modal
227        // can't be dismissed. Escape/backdrop are blocked via the hide.bs.modal guard
228        // below + the existing static backdrop. Default off → normal dismissible modal.
229        var force = !!window._lpForceSelect;
230        modalEl.querySelectorAll('.lp-brandhead__close').forEach(function (b) { b.style.display = force ? 'none' : ''; });
231        var m = getModal(); if (m) m.show(); else modalEl.classList.add('show');
232    }
233    function hideOverlay() { var m = getModal(); if (m) m.hide(); else modalEl.classList.remove('show'); }
234    // Block every close attempt (Escape, programmatic hide) while a selection is forced.
235    modalEl.addEventListener('hide.bs.modal', function (e) { if (window._lpForceSelect) e.preventDefault(); });
236
237    // ─── Step state machine ─────────────────────────────────
238    function showLocationStep(name) {
239        if (state.currentStep && state.currentStep !== name) window._lpPrevStep = state.currentStep;
240        $$('.location-flow-step').forEach(function (s) {
241            if (s.dataset.step === name) s.classList.add('active');
242            else                          s.classList.remove('active');
243        });
244        state.currentStep = name;
245
246        // Expand the dialog only for the two-panel address form.
247        var dlg = modalEl.querySelector('.modal-dialog');
248        if (dlg) dlg.classList.toggle('modal-dialog--wide', name === 'addressForm');
249
250        // Hide the "Already have a Saved Address? Login" CTA on the entry step when the
251        // user is already authenticated — they have direct access to saved addresses.
252        if (name === 'location') {
253            var loginRow = modalEl.querySelector('.alreadyhaveanaccount__cta');
254            if (loginRow) {
255                var u = userData();
256                loginRow.style.display = (u.token && u.contactMappingId) ? 'none' : '';
257            }
258        }
259
260        // Step-specific init
261        if (name === 'savedAddresses') loadSavedAddresses();
262        if (name === 'enterAddress')   initEnterAddressAutocomplete();
263        if (name === 'addressForm') {
264            // Reset right form to hidden + confirm button to disabled on every
265            // entry; user must re-pick the pin and re-confirm.
266            var rightPanel = $('#addressFormRight');
267            if (rightPanel) rightPanel.style.display = '';
268            var confirmCta = modalEl.querySelector('#confirmLocationBtn .btn');
269            if (confirmCta) confirmCta.disabled = true;
270            initAddressMap();
271            initHomeLocalityAutocomplete();
272        }
273        if (name === 'orderType')      loadOrderTypes();
274        if (name === 'redirect')       startRedirectCountdown();
275        if (name === 'location') {
276            var err = $('#error-msg-show'); if (err) err.classList.add('d-none');
277        }
278    }
279
280    function closeLocationFlow() { if (window._lpForceSelect) return; hideOverlay(); }
281
282    // ─── GPS / current location ─────────────────────────────
283    function CurrentAddress() {
284        var err = $('#error-msg-show');
285        if (err) err.classList.add('d-none');
286
287        if (!navigator.geolocation) {
288            if (err) { err.textContent = 'Geolocation not supported by this browser.'; err.classList.remove('d-none'); }
289            return;
290        }
291        if (!window.isSecureContext) {
292            if (err) { err.textContent = 'Location requires a secure (HTTPS) connection.'; err.classList.remove('d-none'); }
293            return;
294        }
295
296        navigator.geolocation.getCurrentPosition(function (pos) {
297            var lat = pos.coords.latitude, lng = pos.coords.longitude;
298            persistCoords(lat, lng, 'gps');
299            // CI3 wla_home_20260507.js:935-956 — wipe the prior outlet/address tuple so the
300            // auto-open guard correctly treats this as "in progress" until orderType is picked.
301            localStorage.removeItem('deliveryAddressId');
302            localStorage.removeItem('selectedDeliveryAddressId');
303            localStorage.removeItem('selectedAddressId');
304            localStorage.removeItem('business');
305            localStorage.removeItem('slug');
306            localStorage.removeItem('formatted_Address');
307            // …and the LABEL of that address. A GPS point has no address type, so leaving
308            // it behind labelled the detected location with the previous saved address's
309            // type — pick "Home", then Use current location, and the chip still read
310            // "Home" over a completely different place. Every other address-wipe block in
311            // this file (resetLocation, the stale-location sweep) already clears this key
312            // next to `userAddress`; this one omitted it.
313            localStorage.removeItem('userAddressLabel');
314            // CI3 wla_home_20260507.js:995-1008 — geocode failure stays on location step with
315            // a retry message; never silently advance to orderType without an address.
316            reverseGeocode(lat, lng).then(function (info) {
317                if (!info) {
318                    if (err) { err.textContent = 'Unable to fetch your location. Please try again or enter manually.'; err.classList.remove('d-none'); }
319                    return;
320                }
321                applyGeocode(info);
322                // Buffer the detected address in memory (applyGeocode no longer persists it).
323                // Nothing touches the outlet's locality/city until the user confirms an
324                // order type (commitAndRedirect).
325                // 'Other' like the locality/pincode paths below: a GPS point has no saved
326                // address type, and leaving it blank made the header inherit the previous
327                // address's label. What matters is that it is always WRITTEN, never inherited.
328                setPendingAddr({ id: '', text: state.address, label: 'Other', locality: state.locality, city: state.city });
329                showLocationStep('orderType');
330            }).catch(function () {
331                if (err) { err.textContent = 'Unable to fetch your location. Please try again or enter manually.'; err.classList.remove('d-none'); }
332            });
333        }, function (e) {
334            var msg = e.code === 1 ? 'Location permission denied. Please enter manually.' : 'Could not get your location. Try entering manually.';
335            if (err) { err.textContent = msg; err.classList.remove('d-none'); }
336        }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 });
337    }
338
339    function applyGeocode(info) {
340        if (!info) return;
341        // CI3 wla_home_20260507.js:987-992 — read locality/city (with resp.data.* fallback),
342        // build address as "locality, city" (or whichever single value is present).
343        var loc  = info.locality || (info.data && info.data.locality) || '';
344        var city = info.city     || (info.data && info.data.city)     || '';
345        var addr = (loc && city) ? (loc + ', ' + city) : (loc || city || '');
346        if (addr) state.address = addr;
347        if (loc)  state.locality = loc;
348        if (city) state.city = city;
349        if (info.state)   state.stateName = info.state;
350        if (info.pincode) state.pinCode  = info.pincode;
351        // In-memory ONLY. Do NOT write userAddress/locality/city to localStorage here — those
352        // keys drive the header's "Selected Outlet" pill (locality+city) and "Your Location"
353        // pill (userAddress). Persisting the user's detected location here overwrote the
354        // still-selected outlet's locality/city on the address form / entry GPS paths (business/
355        // slug stayed pointing at the old outlet), so the outlet pill showed the user's location.
356        // The picked location is buffered via setPendingAddr and committed only on redirect
357        // (flushPendingAddr), after which commitAndRedirect sets locality/city = the OUTLET's.
358    }
359
360    function persistCoords(lat, lng, source) {
361        state.lat = lat; state.lng = lng;
362        localStorage.setItem('userLat', String(lat));
363        localStorage.setItem('userLng', String(lng));
364        localStorage.setItem('userLocSource', source);
365    }
366
367    // ─── Upstream API calls (all hit api.uengage.in with api-key) ────────
368    // Mirrors CI3 wla_home_20260507.js:962-1009 — dual auth: token+contactMappingId for
369    // logged-in, sha256(businessId+salt) hashKey for guests. Server: maps_mongodb.php:294-348.
370    // Note: CI3 sent Token in a request header (same-origin, no preflight). CI4 is cross-origin
371    // (wlp-uat → edge-uat), so a custom Token header triggers a CORS preflight the API rejects.
372    // The server accepts `token` as a query param too (maps_mongodb.php:298 reads it before the
373    // header fallback at :306-310), so we keep auth in the URL and avoid preflight on Token.
374    function reverseGeocode(lat, lng) {
375        var ud = userData();
376        var qs = '?lat=' + lat + '&lng=' + lng + '&businessId=' + pId;
377        if (ud.token && ud.contactMappingId) {
378            qs += '&contactMappingId=' + encodeURIComponent(ud.contactMappingId)
379                + '&token=' + encodeURIComponent(ud.token);
380        } else {
381            var hk = (window.AppConfig && window.AppConfig.geocodeHashKey) || '';
382            qs += '&hashKey=' + encodeURIComponent(hk);
383        }
384        var url = API_BASE + '/maps/getGeocodeAddress' + qs;
385        return fetch(url, { method: 'GET', headers: apiHeaders(), credentials: 'omit' })
386            .then(function (r) { return r.json(); })
387            .then(function (d) { return (d && (d.status === 1 || d.status === '1')) ? d : null; });
388    }
389
390    // Loading row shown in the hybrid results dropdown from the moment a valid (≥3 char)
391    // query is typed until fetchLocality resolves — so the debounce + network wait reads as
392    // "searching", not "no matches". renderLocalityResults overwrites it with the real list.
393    function renderLocalityLoading(el) {
394        if (!el) return;
395        el.innerHTML = '<li class="lp-search-loading">'
396            + '<span class="lp-inline-spinner" aria-hidden="true"></span>Searching\u2026</li>';
397        el.style.display = 'block';
398    }
399
400    var searchTimer = null;
401    function enterAddressSearchLocality(keyword) {
402        // Google flow: places.Autocomplete renders its own .pac-container dropdown when the
403        // SDK is loaded — suppress the AJAX hybrid UI. Falls through if Google auth failed.
404        if (useGoogle && !window._gmapsAuthFailed) return;
405        clearTimeout(searchTimer);
406        var resultsEl = $('#enterAddressLocalityResults');
407        if (!keyword || keyword.length < 3) {
408            if (resultsEl) { resultsEl.style.display = 'none'; resultsEl.innerHTML = ''; }
409            return;
410        }
411        renderLocalityLoading(resultsEl);
412        searchTimer = setTimeout(function () {
413            fetchLocality(keyword).then(function (items) { renderLocalityResults(resultsEl, items, 'enterAddress'); });
414        }, 300);
415    }
416
417    var homeSearchTimer = null;
418    function homeSearchLocality(keyword) {
419        if (useGoogle && !window._gmapsAuthFailed) return;
420        clearTimeout(homeSearchTimer);
421        var resultsEl = $('#homeLocalityResults');
422        if (!keyword || keyword.length < 3) {
423            if (resultsEl) { resultsEl.style.display = 'none'; resultsEl.innerHTML = ''; }
424            return;
425        }
426        renderLocalityLoading(resultsEl);
427        homeSearchTimer = setTimeout(function () {
428            fetchLocality(keyword).then(function (items) { renderLocalityResults(resultsEl, items, 'addressForm'); });
429        }, 300);
430    }
431
432    // ─── Google Places Autocomplete on entry-step inputs ────
433    // Mirrors CI3 footer.php:2155-2210 initEnterAddressAutocomplete. For Google-flow brands
434    // (mapsKey + mapsFlow=1), the entry-step search uses Google Places directly instead of
435    // /client/searchLocality. On place_changed → persist coords/address → orderType step.
436    var _enterAddrAc = null, _homeLocAc = null;
437    function initGoogleAutocomplete(inputId, acRef) {
438        if (!useGoogle || window._gmapsAuthFailed) return;
439        var input = document.getElementById(inputId);
440        if (!input) return;
441        if (acRef.ac) return; // already bound
442        loadGoogleMaps().then(function () {
443            if (!window.google || !google.maps || !google.maps.places) return;
444            acRef.ac = new google.maps.places.Autocomplete(input, {
445                componentRestrictions: { country: 'IN' },
446                fields: ['address_components', 'geometry.location', 'formatted_address', 'name']
447            });
448            acRef.ac.addListener('place_changed', function () {
449                var p = acRef.ac.getPlace();
450                if (!p || !p.geometry || !p.geometry.location) return;
451                var lat = p.geometry.location.lat();
452                var lng = p.geometry.location.lng();
453                persistCoords(lat, lng, 'locality');
454                state.address = p.formatted_address || p.name || '';
455                localStorage.setItem('searchLocalityInput', JSON.stringify([lat, lng]));
456                // CI3 wla_home_20260507.js:939-955 — locality pick = fresh coords; drop stale tuple.
457                localStorage.removeItem('deliveryAddressId');
458                localStorage.removeItem('selectedDeliveryAddressId');
459                localStorage.removeItem('selectedAddressId');
460                localStorage.removeItem('business');
461                localStorage.removeItem('slug');
462                localStorage.removeItem('userAddress');
463                localStorage.removeItem('userAddressLabel');
464                // Buffer the picked locality; committed on redirect (flushPendingAddr).
465                // 'Other' — same product decision as the GPS path above; see that comment.
466                setPendingAddr({ id: '', text: state.address, label: 'Other', locality: '', city: '' });
467                showLocationStep('orderType');
468            });
469        });
470    }
471    function initEnterAddressAutocomplete() { initGoogleAutocomplete('search_locality_new',     { get ac() { return _enterAddrAc; }, set ac(v) { _enterAddrAc = v; } }); }
472    function initHomeLocalityAutocomplete() { initGoogleAutocomplete('homeLocalitySearchInput', { get ac() { return _homeLocAc; },   set ac(v) { _homeLocAc = v; } }); }
473
474    // CI3 footer.php:2212-2228 — if Google Maps key is restricted/rejected for the domain,
475    // drop back to the hybrid AJAX path so the user can still search.
476    window.gm_authFailure = function () {
477        window._gmapsAuthFailed = true;
478        _enterAddrAc = null;
479        _homeLocAc = null;
480    };
481
482    // Hybrid (non-Google) flow: general locality geo-search. Mirrors CI3
483    // ue_silver/home.php:268 → maps_mongodb.php:603 getLocalityByName — a guest,
484    // token-less prefix match over the global maps_places collection that returns
485    // ANY locality + coords. This is NOT /client/searchLocality (which only returns
486    // the brand's own outlets, so typing a real area name gave "No matches").
487    // Server enforces a 3-char minimum and IP rate limiting.
488    function fetchLocality(keyword) {
489        keyword = (keyword || '').trim();
490        if (keyword.length < 3) return Promise.resolve([]);
491        var url = API_BASE + '/maps/getLocalityByName'
492                + '?localityName=' + encodeURIComponent(keyword);
493        return fetch(url, { method: 'GET', headers: apiHeaders(), credentials: 'omit' })
494            .then(function (r) { return r.json(); })
495            .then(function (d) {
496                if (!d || d.status != 1) return [];
497                var items = d.search_results || d.localities || d.data || [];
498                if (!Array.isArray(items)) items = [];
499                return items.map(function (it) {
500                    return {
501                        localityName: it.localityName || it.locality || it.name || '',
502                        cityName:     it.cityName || it.city || '',
503                        latitude:     it.latitude || it.lat || 0,
504                        longitude:    it.longitude || it.lng || it.long || 0,
505                        localityId:   it.localityId || it.id || ''
506                    };
507                }).slice(0, 20);
508            })
509            .catch(function () { return []; });
510    }
511
512    // Location pin for result rows (stroke=currentColor so CSS colours it).
513    var PIN_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>';
514
515    function renderLocalityResults(el, items, ctx) {
516        if (!el) return;
517        if (!items.length) {
518            el.innerHTML = '<li class="lp-result-empty">No matches</li>';
519            el.style.display = 'block';
520            return;
521        }
522        el.innerHTML = items.map(function (it) {
523            var lat = it.latitude || it.lat || 0;
524            var lng = it.longitude || it.lng || 0;
525            return '<li class="lp-result-row" '
526                 + 'data-lat="' + lat + '" data-lng="' + lng + '" '
527                 + 'data-locality="' + (it.localityName || '').replace(/"/g, '&quot;') + '" '
528                 + 'data-city="' + (it.cityName || '').replace(/"/g, '&quot;') + '">'
529                 + '<span class="lp-result-row__pin">' + PIN_ICON + '</span>'
530                 + '<span class="lp-result-row__body">'
531                 +   '<span class="lp-result-row__title">' + (it.localityName || '') + '</span>'
532                 +   (it.cityName ? '<span class="lp-result-row__sub">' + it.cityName + '</span>' : '')
533                 + '</span>'
534                 + '</li>';
535        }).join('');
536        el.style.display = 'block';
537        Array.prototype.forEach.call(el.querySelectorAll('.lp-result-row'), function (li) {
538            li.addEventListener('click', function () { selectLocality(li, ctx); });
539        });
540    }
541
542    function selectLocality(li, ctx) {
543        var lat = parseFloat(li.dataset.lat);
544        var lng = parseFloat(li.dataset.lng);
545        if (!isFinite(lat) || !isFinite(lng) || (lat === 0 && lng === 0)) return;
546        persistCoords(lat, lng, 'address');
547        state.locality = li.dataset.locality || '';
548        state.city     = li.dataset.city || '';
549        // In-memory ONLY — do NOT persist locality/city here. They drive the header's
550        // "Selected Outlet" pill, which must keep showing the already-selected outlet until
551        // the user confirms a new one. addressForm ctx reflects state via applyGeocodeToForm;
552        // entry ctx buffers via setPendingAddr below. commitAndRedirect writes the outlet's
553        // locality/city at confirmation.
554
555        if (ctx === 'addressForm') {
556            applyMapCenter(lat, lng);
557            state.lat = lat; state.lng = lng;
558            // The picked locality is the reference label shown in the right-panel info box.
559            state.address = state.locality + (state.city ? ', ' + state.city : '');
560            // Funnel through the shared hydrator — same path as a pin-drop / Google
561            // place_changed — so the floating right-side info box (#homeAddrFormInfo) AND the
562            // visible/hidden Locality/City/Lat/Lng fields update and Save unlocks. The hybrid
563            // branch previously hand-set only a subset of inputs and never touched the info box,
564            // so the displayed address didn't change on select.
565            applyGeocodeToForm(lat, lng);
566            var rl = $('#homeLocalityResults'); if (rl) { rl.style.display = 'none'; rl.innerHTML = ''; }
567            // Show the SELECTED locality in the search box (state.address is exactly
568            // that label, built just above). It used to be cleared, leaving nothing on
569            // screen to tell a wrong pick from a right one; the typed fragment is not
570            // the useful reference either, the chosen place is.
571            var si = $('#homeLocalitySearchInput'); if (si && state.address) si.value = state.address;
572        } else {
573            // Entry-step locality pick — mirror CI3 selectHybridLocality (ue_silver/home.php
574            // → showOrderTypeSelector) and the Google-flow place_changed handler: a picked
575            // locality resolves coords and advances straight to the order-type selector
576            // (find nearest outlet), NOT the add-address form. The full delivery address is
577            // collected later. Previously this opened addressForm, diverging from both flows.
578            state.address = state.locality + (state.city ? ', ' + state.city : '');
579            localStorage.setItem('searchLocalityInput', JSON.stringify([lat, lng]));
580            localStorage.removeItem('deliveryAddressId');
581            localStorage.removeItem('selectedDeliveryAddressId');
582            localStorage.removeItem('selectedAddressId');
583            localStorage.removeItem('business');
584            localStorage.removeItem('slug');
585            localStorage.removeItem('userAddress');
586            localStorage.removeItem('userAddressLabel');
587            // 'Other' — same product decision as the GPS path; see that comment.
588            setPendingAddr({ id: '', text: state.address, label: 'Other', locality: state.locality, city: state.city });
589            showLocationStep('orderType');
590        }
591    }
592
593    // ─── Address form / map ─────────────────────────────────
594    // Tracks an in-progress edit from the Manage Addresses page. When set, the
595    // upstream POST carries deliveryAddressId (turning add → update) and the
596    // post-save flow dispatches `lp:address-saved` instead of redirecting.
597    var editingAddressId = 0;
598    var manageMode       = false;
599
600    // `manage` = opened standalone from the Manage Addresses page (not the home/product
601    // find-outlet flow). In that mode the form closes fully and save dispatches
602    // `lp:address-saved` instead of advancing to the order-type step.
603    function openHomeAddressForm(manage) {
604        editingAddressId = 0;
605        manageMode = !!manage;
606        // Wipe ALL carried-over state + form fields BEFORE the map inits. `state` is
607        // module-level and the hidden lat/lng + street/email inputs are never cleared by
608        // the browser between opens, so a prior add/edit (or saved-address pick) left its
609        // coords and text in place — and saveHomeAddress read them verbatim, saving the
610        // PREVIOUS location for a brand-new address. Resetting state.lat/lng here also makes
611        // initAddressMap fall back to localStorage (last-known area) instead of re-hydrating
612        // the stale in-memory coords. CI3 normal-flow default copy.
613        state.lat = null; state.lng = null;
614        state.address = ''; state.locality = ''; state.city = '';
615        state.stateName = ''; state.pinCode = '';
616        state.savePromise = null;
617        ['#homeAddrLine1', '#homeAddrLine2', '#homeAddrPhone', '#homeAddrEmail',
618         '#homeAddrLat', '#homeAddrLng',
619         '#homeAddrLocality', '#homeAddrCity', '#homeAddrState', '#homeAddrPincode',
620         '#homeAddressSearch'].forEach(function (sel) {
621            var elx = $(sel); if (elx) elx.value = '';
622        });
623        // Hide the resolved-address info boxes so a prior address isn't shown until the new
624        // geocode resolves, and default the type radio back to Home (an edit may have moved it).
625        ['#homeAddrMapInfo', '#homeAddrFormInfo'].forEach(function (sel) {
626            var box = $(sel); if (box) box.style.display = 'none';
627        });
628        var homeRadio = $('#homeRadio1'); if (homeRadio) homeRadio.checked = true;
629        showLocationStep('addressForm');
630        showOverlay();
631        prefillPhoneFromUserdata();
632        homeSetSaveButtonState(false, 'Select location on map to continue');
633        // CI3 wla_home_20260507.js:1636-1653 — auto-fetch GPS only when we don't already
634        // have stored coords. Silently no-ops on permission denial; user can still search
635        // or click "Go to current location" manually.
636        var hasStored = localStorage.getItem('userLat') && localStorage.getItem('userLng');
637        if (!hasStored) homeAddrGoToCurrentLocation();
638    }
639
640    // CI3 wla_home_20260507.js:1447-1459 — strip optional +91/91, prefill only on a 10-digit
641    // number. Skip if the user already typed something so we don't clobber manual input.
642    function prefillPhoneFromUserdata() {
643        var phoneEl = $('#homeAddrPhone');
644        if (!phoneEl || phoneEl.value) return;
645        var ud = userData();
646        var raw = String(ud.mobile || ud.mobileNo || '').replace(/^\+?91/, '');
647        if (/^\d{10}$/.test(raw)) phoneEl.value = raw;
648    }
649
650    // Wait for the address-form map to finish initializing (Google Maps loads async).
651    // Polls for ~6s so a GPS resolution that beats the map can still center it.
652    function whenMapReady(cb) {
653        if (mapInstance.map) { cb(); return; }
654        var tries = 0;
655        var iv = setInterval(function () {
656            if (mapInstance.map) { clearInterval(iv); cb(); }
657            else if (++tries > 30) { clearInterval(iv); }
658        }, 200);
659    }
660
661    // CI3 wla_home_20260507.js:1678-1690 — gate the Save Address CTA on the map-pin
662    // lifecycle. Same state machine as the checkout add-address modal:
663    //   • "Select location on map to continue"  — initial / pin not yet placed
664    //   • "Save Address"  (enabled)             — pin placed + locality/city resolved
665    //   • "Location not serviceable"            — geocode returned status != 1
666    function homeSetSaveButtonState(enabled, message) {
667        var btn = $('#homeAddrSaveBtn');
668        var err = $('#homeAddrError');
669        if (btn) {
670            btn.disabled = !enabled;
671            btn.style.opacity = enabled ? '1' : '0.5';
672        }
673        if (err) {
674            if (enabled || !message) { err.classList.add('d-none'); err.classList.remove('is-loading'); err.textContent = ''; }
675            else                     { err.classList.remove('d-none', 'is-loading'); err.style.color = ''; err.textContent = message; }
676        }
677    }
678
679    // Show a neutral "Locating your address…" loader in the address-form footer while the
680    // pin's coords are reverse-geocoded (Google client Geocoder OR hybrid getGeocodeAddress).
681    // Save stays disabled meanwhile; applyGeocodeToForm (success) or the not-serviceable
682    // message (failure) replaces this, so callers must clear it on every resolution path.
683    function homeSetGeocoding() {
684        var btn = $('#homeAddrSaveBtn');
685        var cta = $('#confirmLocationBtn .btn');
686        var err = $('#homeAddrError');
687        if (btn) { btn.disabled = true; btn.style.opacity = '0.5'; }
688        if (cta) cta.disabled = true;
689        if (err) {
690            err.classList.remove('d-none');
691            err.classList.add('is-loading');
692            err.style.color = '';
693            err.innerHTML = '<span class="lp-inline-spinner" aria-hidden="true"></span>Locating your address\u2026';
694        }
695    }
696
697    // Re-render the map after a container resize WITHOUT the resize's own moveend/idle
698    // re-geocoding. invalidateSize preserves the centre, but getCenter() jitters ~1e-5 after a
699    // resize — past the dedupe — so the move handler would fire a second, redundant geocode of
700    // the same point (the "2 calls / fluctuating between 2 addresses" bug). The flag makes the
701    // move handlers skip that one programmatic move.
702    function resizeHomeMap() {
703        if (!mapInstance.map) return;
704        mapInstance.suppressGeo = true;
705        if (mapInstance.kind === 'leaflet' && mapInstance.map.invalidateSize) mapInstance.map.invalidateSize(); // default pan:true PRESERVES centre
706        else if (mapInstance.kind === 'google' && window.google && window.google.maps && window.google.maps.event) window.google.maps.event.trigger(mapInstance.map, 'resize');
707        setTimeout(function () { mapInstance.suppressGeo = false; }, 300);
708    }
709
710    // Shared form-field hydration after a geocode resolves. Used by both onPinChanged
711    // (map drag) and homeAddrGoToCurrentLocation (button click + auto-GPS on open) so
712    // locality/city/state/pincode land in the hidden inputs and the confirm CTA activates.
713    // (Locality/City have no visible field — they show in the green card — but their hidden
714    // inputs #homeAddrLocality / #homeAddrCity below still feed the save payload.)
715    function applyGeocodeToForm(lat, lng) {
716        $('#homeAddrLat') && ($('#homeAddrLat').value = lat);
717        $('#homeAddrLng') && ($('#homeAddrLng').value = lng);
718        $('#homeAddrCity') && ($('#homeAddrCity').value = state.city || '');
719        $('#homeAddrState') && ($('#homeAddrState').value = state.stateName || '');
720        $('#homeAddrPincode') && ($('#homeAddrPincode').value = state.pinCode || '');
721        $('#homeAddrLocality') && ($('#homeAddrLocality').value = state.locality || '');
722        // Reference shows on both screens: the map step (#homeAddrMapInfo, above
723        // Confirm Location) and the form step (#homeAddrFormInfo, under the header).
724        var loc = state.locality || '';
725        var addr = state.address || '';
726        // Green card + map card show "locality, city" — but only append city when the locality
727        // doesn't already contain it (avoids "Kolkata, Kolkata" / "Panchkula, Panchkula").
728        var cityName = state.city || '';
729        var locWithCity = (cityName && loc.toLowerCase().indexOf(cityName.toLowerCase()) === -1)
730            ? (loc ? (loc + ', ' + cityName) : cityName) : loc;
731        var info1 = $('#homeAddrMapLocality');     if (info1) info1.textContent = locWithCity;
732        var info2 = $('#homeAddrMapFullAddress');
732  if (info2) info2.textContent = addr;
733        var info3 = $('#homeAddrFormLocality');    if (info3) info3.textContent = locWithCity;
734        var info4 = $('#homeAddrFormFullAddress'); if (info4) info4.textContent = addr;
735        var box  = $('#homeAddrMapInfo');  if (box)  box.style.display = 'block';
736        var box2 = $('#homeAddrFormInfo'); if (box2) box2.style.display = 'block';
737        // Showing the below-map card shrinks the map (flex-fill layout) — re-render so it
738        // doesn't show gray tiles, WITHOUT the resize's moveend re-geocoding the same point.
739        setTimeout(resizeHomeMap, 80);
740        var cta = $('#confirmLocationBtn .btn'); if (cta) cta.disabled = false;
741        if (state.locality && state.city) homeSetSaveButtonState(true,  'Save Address');
742        else                              homeSetSaveButtonState(false, 'Select location on map to continue');
743    }
744
745    // CI3 wla_home_20260507.js:1714-1724 — parse a Google Places/Geocoder
746    // address_components array into state. City = the 'locality' type; Locality =
747    // 'sublocality' (falls back to city); plus admin_area_1 → state, postal_code → pincode.
748    function applyAddressComponents(components) {
749        if (!components || !components.length) return;
750        var city = '', stateName = '', pincode = '', locality = '';
751        components.forEach(function (c) {
752            var t = c.types || [];
753            if (t.indexOf('locality') > -1)                    city     = c.long_name;
754            if (t.indexOf('administrative_area_level_1') > -1) stateName = c.long_name;
755            if (t.indexOf('postal_code') > -1)                 pincode  = c.long_name;
756            if (t.indexOf('sublocality_level_1') > -1 || t.indexOf('sublocality') > -1) locality = c.long_name;
757        });
758        state.city     = city;
759        state.locality = locality || city;
760        if (stateName) state.stateName = stateName;
761        if (pincode)   state.pinCode   = pincode;
762    }
763
764    // CI3 wla_home_20260507.js:1703-1738 (updateHomeAddressFromLatLng) — the single
765    // reverse-geocode entry point for the address form, shared by search, pin drag, and
766    // the GPS button. Google-flow brands use the client-side Geocoder and add the address
767    // UNCONDITIONALLY (no serviceability call); serviceability is decided later at checkout
768    // when the nearest open outlet is resolved. Non-Google (Leaflet) brands have no client
769    // geocoder, so they fall back to the AJAX getGeocodeAddress — their only option.
770    function hydrateFormFromLatLng(lat, lng) {
771        state.lat = lat; state.lng = lng;
772        homeSetGeocoding();
773        if (useGoogle && !window._gmapsAuthFailed && window.google && google.maps && google.maps.Geocoder) {
774            var geocoder = new google.maps.Geocoder();
775            geocoder.geocode({ location: { lat: lat, lng: lng } }, function (results, status) {
776                if (status !== 'OK' || !results[0]) { homeSetSaveButtonState(false, 'Location not serviceable'); return; }
777                applyAddressComponents(results[0].address_components);
778                state.address = results[0].formatted_address || state.address;
779                if (state.address) localStorage.setItem('userAddress', state.address);
780                // The search box is LEFT ALONE. It used to be cleared here so it would
781                // not sit prefilled with a resolved address — but this runs on every
782                // geocode, including the one fired when the form opens, so anyone who
783                // started typing while the map was still loading had their text wiped
784                // out from under them. It is the user's input, not a label: only they
785                // clear it.
786                applyGeocodeToForm(lat, lng);
787            });
788            return;
789        }
790        reverseGeocode(lat, lng).then(function (info) {
791            if (!info) { homeSetSaveButtonState(false, 'Location not serviceable'); return; }
792            applyGeocode(info);
793            applyGeocodeToForm(lat, lng);
794        });
795    }
796
797    function openEditAddressForm(addr) {
798        if (!addr || typeof addr !== 'object') return;
799        editingAddressId = parseInt(addr.id || addr.deliveryAddressId || 0, 10) || 0;
800        manageMode = true;
801
802        // Open the modal first so the form fields exist in the DOM
803        showLocationStep('addressForm');
804        showOverlay();
805
806        // Pre-fill — fields populated by initAddressMap may overwrite some of these,
807        // so apply on the next tick to win over the initial centering.
808        setTimeout(function () {
809            var setVal = function (sel, v) { var el = $(sel); if (el) el.value = (v == null ? '' : v); };
810            setVal('#homeAddrLine1', addr.addressLine1 || addr.line1 || '');
811            setVal('#homeAddrLine2', addr.addressLine2 || addr.line2 || '');
812            setVal('#homeAddrPhone', addr.mobile       || addr.phone || '');
813            setVal('#homeAddrEmail', addr.email        || '');
814            // Seed in-memory state from the saved address so the shared hydrator below
815            // can fill the form from it. state.address is the reference label shown in the
816            // under-map / under-header info boxes.
817            state.locality  = addr.locality || '';
818            state.city      = addr.city || '';
819            state.stateName = addr.state || addr.stateName || '';
820            state.pinCode   = addr.pinCode || addr.pincode || '';
821            // Info box shows locality (bold) + this sub-line. Keep it to city only — not the
822            // full street address.
823            state.address   = addr.city || '';
824            var lat = parseFloat(addr.latitude  || addr.lat || 0);
825            var lng = parseFloat(addr.longitude || addr.lng || 0);
826            if (lat && lng) { state.lat = lat; state.lng = lng; applyMapCenter(lat, lng); }
827            // Reuse the shared hydrator instead of hand-filling fields. It populates the
828            // right-panel inputs (visible + hidden Locality/City/State/Pincode/Lat/Lng) AND
829            // the under-map / under-header info boxes (#homeAddrMapInfo / #homeAddrFormInfo)
830            // — which openEditAddressForm previously never touched, so the locality + full
831            // address stayed hidden below the map on edit — and unlocks Save.
832            applyGeocodeToForm(state.lat || lat || 0, state.lng || lng || 0);
833            // Address type radio (Home/Office/Hotel/Other) — resolve the same way the list does
834            // (handles numeric addressType/name + custom addressValue labels), then fall back to
835            // 'Other' if the resolved label isn't one of the four standard radios.
836            var typeName = resolveAddressLabel(addr);
837            if (['Home', 'Office', 'Hotel', 'Other'].indexOf(typeName) === -1) typeName = 'Other';
838            var typeRadio = modalEl.querySelector('input[name="homeAddrType"][value="' + typeName + '"]');
839            if (typeRadio) typeRadio.checked = true;
840        }, 0);
841    }
842
843    // Geolocation failures show on the MAP step (#homeLocateError), not the form banner —
844    // on mobile the "Use my location" button lives on the map step, so the message must too.
845    function homeShowError(msg) {
846        var box = $('#homeLocateError');
847        if (!box) return;
848        box.hidden = false;
849        box.textContent = msg;
850    }
851
852    // showError: true when triggered by the manual "Use my location" button — surfaces a
853    // reason on failure (denied permission / insecure-context block) inline instead of
854    // silently no-op'ing. The auto-call on form open (openHomeAddressForm) passes nothing → quiet.
855    function homeAddrGoToCurrentLocation(showError) {
856        var lerr = $('#homeLocateError'); if (lerr) lerr.hidden = true; // clear any prior failure
857        if (!navigator.geolocation) {
858            if (showError) homeShowError('Location services are not available in this browser.');
859            return;
860        }
861        navigator.geolocation.getCurrentPosition(function (pos) {
862            var lat = pos.coords.latitude, lng = pos.coords.longitude;
863            persistCoords(lat, lng, 'gps');
864            // When called from openHomeAddressForm the map is still loading, so defer the
865            // center + reverse-geocode until it's ready. For a manual click the map is
866            // already up and whenMapReady invokes the callback synchronously.
867            // CI3 wla_home_20260507.js:1639-1650 — auto-GPS on the address form hydrates via
868            // the same path as search/drag (Google client geocoder, no add-time gate).
869            whenMapReady(function () {
870                applyMapCenter(lat, lng);
871                hydrateFormFromLatLng(lat, lng);
872            });
873        }, function () {
874            if (showError) homeShowError('Unable to get your location. Please allow location access — this needs a secure (HTTPS) page.');
875        }, { enableHighAccuracy: true, timeout: 10000 });
876    }
877
878    function confirmLocationAndShowForm() {
879        // Mobile two-step flow: hide map, reveal the form panel. Desktop ignores
880        // the class (panels are side-by-side via CSS).
881        var container = modalEl.querySelector('.homeAddressFormModal .addadress-starting');
882        if (container) container.classList.add('is-form-visible');
883    }
884
885    function backToLocationMap() {
886        var container = modalEl.querySelector('.homeAddressFormModal .addadress-starting');
887        if (container) container.classList.remove('is-form-visible');
888    }
889
890    // Green card's "Change on map" — mirrors checkout's addressForm.js: on mobile swap
891    // back to the map screen, then focus + flash the map search field so the user can re-pick.
892    function homeChangeOnMap() {
893        backToLocationMap();
894        var input = $('#homeAddressSearch') || $('#homeLocalitySearchInput');
895        var wrap  = input && input.closest('.map-search');
896        setTimeout(function () {
897            if (input) input.focus();
898            if (!wrap) return;
899            wrap.classList.add('map-search--flash');
900            setTimeout(function () { wrap.classList.remove('map-search--flash'); }, 1200);
901        }, 60);
902    }
903
904    // Close (X) handler for the address-form step. On the Manage Addresses page the form is
905    // opened standalone (manageMode), so closing fully dismisses the modal — it must NOT fall
906    // back into the saved-addresses / find-outlet flow, which is restricted to home + product
907    // pages. In the home/product flow the form is reached from savedAddresses, so it returns there.
908    function closeAddressForm() {
909        if (manageMode) closeLocationFlow();
910        else            showLocationStep('savedAddresses');
911    }
912
913    function saveHomeAddress() {
914        var ud = userData();
915        var errEl = $('#homeAddrError');
916        if (errEl) errEl.classList.add('d-none');
917
918        var line1 = ($('#homeAddrLine1') || {}).value || '';
919        var line2 = ($('#homeAddrLine2') || {}).value || '';
920        var phone = ($('#homeAddrPhone') || {}).value || '';
921        var email = ($('#homeAddrEmail') || {}).value || '';
922        var typeEl = modalEl.querySelector('input[name="homeAddrType"]:checked');
923        var typeVal = typeEl ? typeEl.value : 'Home';
924        var lat = parseFloat(($('#homeAddrLat') || {}).value || '') || state.lat || 0;
925        var lng = parseFloat(($('#homeAddrLng') || {}).value || '') || state.lng || 0;
926
927        line1 = line1.trim();
928        phone = phone.trim();
929        if (line1.length < 3) {
930            if (errEl) { errEl.textContent = 'Please enter a valid house / flat / block.'; errEl.classList.remove('d-none', 'is-loading'); }
931            return;
932        }
933        if (!/^[0-9]{10}$/.test(phone)) {
934            if (errEl) { errEl.textContent = 'Please enter a valid 10-digit phone number.'; errEl.classList.remove('d-none', 'is-loading'); }
935            return;
936        }
937        if (!lat || !lng) {
938            if (errEl) { errEl.textContent = 'Please pick a location on the map.'; errEl.classList.remove('d-none', 'is-loading'); }
939            return;
940        }
941
942        var locality = state.locality || (($('#homeAddrLocality') || {}).value || '');
943        var city     = state.city || (($('#homeAddrCity') || {}).value || '');
944        if (!locality.trim() || !city.trim()) {
945            if (errEl) { errEl.textContent = 'Please pick a serviceable location on the map.'; errEl.classList.remove('d-none', 'is-loading'); }
946            return;
947        }
948        var fullAddr = [line1, line2, locality, city].filter(Boolean).join(', ');
949        // Explicit user confirmation — persist coords now (needed for the nearest-outlet
950        // lookup). The address identity is held in memory until redirect (flushPendingAddr),
951        // so adding an address that turns out non-serviceable never leaks onto checkout.
952        persistCoords(lat, lng, 'address');
953        if (!manageMode) {
954            setPendingAddr({ id: editingAddressId || '', text: fullAddr, label: typeVal, locality: locality, city: city });
955        }
956
957        // Save to upstream only for logged-in users (CI3 contract).
958        // CI3 reuses /client/addDeliveryAddress for BOTH add and update — passing
959        // deliveryAddressId in the body switches it to update mode.
960        if (ud.token && ud.contactMappingId) {
961            var url = API_BASE + '/client/addDeliveryAddress/' + pId + '/' + encodeURIComponent(ud.contactMappingId) + '/' + encodeURIComponent(ud.token);
962            var form = new URLSearchParams();
963            form.set('addressLine1', line1);
964            form.set('addressLine2', line2);
965            form.set('locality', locality);
966            form.set('city', city);
967            form.set('state', state.stateName || '');
968            form.set('pinCode', state.pinCode || '');
969            form.set('mobile', phone);
970            form.set('email', email);
971            form.set('name', typeVal);
972            form.set('type', typeVal);
973            form.set('latitude', String(lat));
974            form.set('longitude', String(lng));
975            if (ud.contactId) form.set('contactId', String(ud.contactId));
976            if (editingAddressId) form.set('deliveryAddressId', String(editingAddressId));
977            var wasManage  = manageMode;
978            // Snapshot the edit-id: this callback is async and editingAddressId is reset
979            // synchronously below, so read the captured value, not the live module var.
980            var wasEditId  = editingAddressId;
981            state.savePromise = fetch(url, {
982                method: 'POST',
983                headers: apiHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }),
984                body: form.toString(),
985                credentials: 'omit'
986            }).then(function (r) { return r.json(); })
987              .then(function (d) {
988                  // Upstream id shape varies by path — accept rows.deliveryAddressId / rows.id /
989                  // top-level deliveryAddressId / data.deliveryAddressId. Falls back to the edited
990                  // id on an update.
991                  var id = (d && ((d.rows && (d.rows.deliveryAddressId || d.rows.id))
992                                  || d.deliveryAddressId
993                                  || (d.data && d.data.deliveryAddressId))) || wasEditId;
994                  // Attach the saved id to the buffered selection — still NOT written to the
995                  // checkout keys; that happens on redirect via flushPendingAddr.
996                  if (id && !wasManage && state.pendingAddr) state.pendingAddr.id = id;
997                  if (wasManage) {
998                      window.dispatchEvent(new CustomEvent('lp:address-saved'));
999                  }
1000              }).catch(function () {});
1001        }
1002
1003        // Reset the edit-id after every save (not just the manage branch) so a subsequent
1004        // "Add new address" can't inherit the id of the address just edited.
1005        editingAddressId = 0;
1006        if (manageMode) {
1007            // Manage Addresses flow — close the modal, don't redirect to order type.
1008            manageMode = false;
1009            hideOverlay();
1010        } else {
1011            showLocationStep('orderType');
1012        }
1013    }
1014
1015    // ─── Saved addresses ────────────────────────────────────
1016    function openLoginOrSavedAddresses() {
1017        var ud = userData();
1018        if (!ud.token || !ud.contactMappingId) {
1019            // Close the location modal first so the login modal isn't stacked on top of it.
1020            // Hook _onLoginSuccess so we re-open at savedAddresses once auth completes.
1021            hideOverlay();
1022            window._onLoginSuccess = function () {
1023                window._onLoginSuccess = null;
1024                showLocationStep('savedAddresses');
1025                showOverlay();
1026            };
1027            if (typeof window.showLogin === 'function') {
1028                // Small delay so Bootstrap can finish hiding the location modal before showing the next.
1029                setTimeout(function () { window.showLogin(); }, 200);
1030            }
1031            return;
1032        }
1033        showLocationStep('savedAddresses');
1034    }
1035
1036    // Numeric type-id → label. Mirrors checkout.js:2009 ADDR_TYPE so both screens agree.
1037    var TYPE_LABELS = { 1: 'Home', 2: 'Office', 3: 'Hotel', 4: 'Other' };
1038    // String type-name → canonical label (case-insensitive). 'work' is checkout's alias for office.
1039    var TYPE_NAMES  = { home: 'Home', office: 'Office', work: 'Office', hotel: 'Hotel', other: 'Other', others: 'Other' };
1040    var TYPE_ICONS = {
1041        Home:   '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-7 9 7v9a1 1 0 01-1 1h-5v-6h-6v6H4a1 1 0 01-1-1v-9z"/></svg>',
1042        Office: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="18" height="13" rx="1.5"/><path d="M8 7V5a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M3 13h18"/></svg>',
1043        Hotel:  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 18v-6h14a4 4 0 014 4v2"/><path d="M2 18h20"/><path d="M2 18v3"/><path d="M22 18v3"/><circle cx="7" cy="11" r="2.2"/></svg>',
1044        Other:  '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>'
1045    };
1046    var CHEV_RIGHT = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M9 18l6-6-6-6"/></svg>';
1047
1048    function resolveAddressLabel(a) {
1049        // The address type lives in `name` — a numeric id ("1".."4"), a string label ("Office"),
1050        // or null. checkout.js:2106 reads the SAME field via addrTypeLabel(addr.name); this list
1051        // ignored `name`, so every row fell through to 'Home'. `addressType`/`addressValue` are
1052        // noisy in production (zero-padded "00000000000", literal "0"), so they're a last resort.
1053        var candidates = [a.name, a.addressType, a.addressValue, a.type];
1054        for (var i = 0; i < candidates.length; i++) {
1055            var v = String(candidates[i] == null ? '' : candidates[i]).trim();
1056            if (!v) continue;
1057            if (/^\d+$/.test(v)) {                 // numeric id (incl. zero-padded) — 0 = unset
1058                var n = parseInt(v, 10);
1059                if (n !== 0 && TYPE_LABELS[n]) return TYPE_LABELS[n];
1060                continue;
1061            }
1062            return TYPE_NAMES[v.toLowerCase()] || v; // known label, else a genuine custom label
1063        }
1064        return 'Home';
1065    }
1066
1067    function iconFor(label) {
1068        return TYPE_ICONS[label] || TYPE_ICONS.Other;
1069    }
1070
1071    function escAttr(s) { return String(s == null ? '' : s).replace(/"/g, '&quot;'); }
1072    function escHtml(s) {
1073        return String(s == null ? '' : s)
1074            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1075    }
1076
1077    function cleanFieldList(arr) {
1078        // Drop empty strings, "0" placeholders, and any numeric-zero stand-ins from the joined address.
1079        return arr.filter(function (v) {
1080            if (v === null || v === undefined) return false;
1081            var s = String(v).trim();
1082            if (!s) return false;
1083            if (s === '0' || s === '0.0') return false;
1084            return true;
1085        });
1086    }
1087
1088    // Saved-address pagination: show the first 3, then reveal 4 more per
1089    // "View More" click (CI3 home-flow chunking behaviour).
1090    var SAVED_ADDR_INITIAL = 3;
1091    var SAVED_ADDR_CHUNK   = 4;
1092
1093    function savedAddrRowHtml(a) {
1094        var label = resolveAddressLabel(a);
1095        var full  = cleanFieldList([a.addressLine1, a.addressLine2, a.locality, a.city, a.pinCode]).join(', ');
1096        // getDeliveryAddress returns coords as latitude/longitude OR lat/lng depending on the
1097        // upstream path — accept both (manageAddresses.js normalizes the same way). Without the
1098        // lat/lng fallback, coords-bearing rows render empty → onPickSavedAddress diverts to the
1099        // address step and getNearestOpenOutlet never fires.
1100        var aLat = a.latitude  || a.lat || '';
1101        var aLng = a.longitude || a.lng || '';
1102        return '<div class="saved-addr-row" '
1103            + 'data-id="'       + escAttr(a.deliveryAddressId || a.id || '') + '" '
1104            + 'data-lat="'      + escAttr(aLat) + '" '
1105            + 'data-lng="'      + escAttr(aLng) + '" '
1106            + 'data-locality="' + escAttr(a.locality || '') + '" '
1107            + 'data-city="'     + escAttr(a.city || '') + '" '
1108            + 'data-address="'  + escAttr(full) + '" '
1109            + 'data-label="'    + escAttr(label) + '">'
1110            + '<span class="saved-addr-row__icon">' + iconFor(label) + '</span>'
1111            + '<div class="saved-addr-row__main">'
1112            +   '<span class="saved-addr-row__label">' + escHtml(label) + '</span>'
1113            +   '<span class="saved-addr-row__text" title="' + escAttr(full) + '">' + escHtml(full) + '</span>'
1114            + '</div>'
1115            + '<span class="saved-addr-row__chev">' + CHEV_RIGHT + '</span>'
1116            + '</div>';
1117    }
1118
1119    // Bind the click handler to any saved-addr rows that aren't wired up yet.
1120    function bindSavedAddrRows(listEl) {
1121        Array.prototype.forEach.call(listEl.querySelectorAll('.saved-addr-row'), function (row) {
1122            if (row.dataset.bound === '1') return;
1123            row.dataset.bound = '1';
1124            row.addEventListener('click', function () { onPickSavedAddress(row); });
1125        });
1126    }
1127
1128    // Render `shown` of `rows` plus a toggle link: "View More" reveals the
1129    // next chunk, "View Less" (once all are shown) collapses back to the
1130    // initial count.
1131    function renderSavedAddresses(listEl, rows, shown) {
1132        listEl.innerHTML = rows.slice(0, shown).map(savedAddrRowHtml).join('');
1133        bindSavedAddrRows(listEl);
1134        if (rows.length <= SAVED_ADDR_INITIAL) return;
1135
1136        var toggle = document.createElement('div');
1137        toggle.className = 'saved-addr-more';
1138        toggle.id = 'savedAddrViewMore';
1139        var allShown = shown >= rows.length;
1140        toggle.textContent = allShown ? 'View Less' : 'View More';
1141        toggle.addEventListener('click', function () {
1142            var next = allShown
1143                ? SAVED_ADDR_INITIAL
1144                : Math.min(shown + SAVED_ADDR_CHUNK, rows.length);
1145            renderSavedAddresses(listEl, rows, next);
1146        });
1147        listEl.appendChild(toggle);
1148    }
1149
1150    function loadSavedAddresses() {
1151        var ud = userData();
1152        // Personalise the "Welcome Back" greeting with the user's first name (client-side
1153        // data, so it can't be rendered by PHP). Falls back to a plain "Welcome Back".
1154        var nameEl = $('#lpWelcomeName');
1155        if (nameEl) {
1156            var first = String(ud.name || '').trim().split(/\s+/)[0] || '';
1157            // Suppress empty/null and the generic "user" placeholder — greet by name only
1158            // when it's a real given name.
1159            if (first.toLowerCase() === 'user') first = '';
1160            nameEl.textContent = first ? (', ' + first) : '';
1161        }
1162        var listEl = $('#savedaddress_list');
1163        if (!listEl) return;
1164        if (!ud.token || !ud.contactMappingId) {
1165            listEl.innerHTML = '<div class="saved-addr-empty">Please log in to see saved addresses.</div>';
1166            return;
1167        }
1168        listEl.innerHTML = '<div class="saved-addr-empty">Loading...</div>';
1169        var url = API_BASE + '/client/getDeliveryAddress/' + pId + '/' + encodeURIComponent(ud.contactMappingId) + '/' + encodeURIComponent(ud.token);
1170        fetch(url, { method: 'GET', headers: apiHeaders(), credentials: 'omit' })
1171            .then(function (r) { return r.json(); })
1172            .then(function (d) {
1173                var rows = (d && (d.data || d.rows)) || [];
1174                rows = rows.filter(function (a) {
1175                    return cleanFieldList([a.addressLine1, a.addressLine2, a.locality, a.city, a.pinCode]).length > 0;
1176                });
1177                if (!rows.length) {
1178                    listEl.innerHTML = '<div class="saved-addr-empty">No saved addresses found</div>';
1179                    return;
1180                }
1181                // Show the first SAVED_ADDR_INITIAL rows, then reveal
1182                // SAVED_ADDR_CHUNK more per "View More" click. Once everything
1183                // is visible a "View Less" link collapses back (CI3 chunking).
1184                renderSavedAddresses(listEl, rows, Math.min(SAVED_ADDR_INITIAL, rows.length));
1185            })
1186            .catch(function () {
1187                listEl.innerHTML = '<div class="saved-addr-empty">Could not load addresses.</div>';
1188            });
1189    }
1190
1191    function onPickSavedAddress(row) {
1192        var lat = parseFloat(row.dataset.lat);
1193        var lng = parseFloat(row.dataset.lng);
1194        var id  = row.dataset.id;
1195        // Hold the selection in memory only — committed on redirect (flushPendingAddr).
1196        setPendingAddr({
1197            id:       id,
1198            text:     row.dataset.address || '',
1199            label:    row.dataset.label || 'Home',
1200            locality: row.dataset.locality || '',
1201            city:     row.dataset.city || ''
1202        });
1203
1204        if (isFinite(lat) && isFinite(lng) && lat !== 0 && lng !== 0) {
1205            persistCoords(lat, lng, 'address');
1206            state.locality = row.dataset.locality || '';
1207            state.city     = row.dataset.city || '';
1208            showLocationStep('orderType');
1209        } else {
1210            showLocationStep('enterAddress');
1211        }
1212    }
1213
1214    // ─── Order type / nearest store ─────────────────────────
1215    function loadOrderTypes() {
1216        var lat = state.lat || parseFloat(localStorage.getItem('userLat') || '0');
1217        var lng = state.lng || parseFloat(localStorage.getItem('userLng') || '0');
1218        if (!isFinite(lat) || !isFinite(lng) || (lat === 0 && lng === 0)) {
1219            // No coords — but we may already know WHICH outlet this is, from the
1220            // menu page the visitor is coming off (_seedCurrentOutlet). The
1221            // coordinate lookup below exists to answer "which outlet serves you";
1222            // that question is already answered, so ask the one that is actually
1223            // open instead: how do you want it. This is the menu page's own
1224            // switcher behaviour, reached from every other page.
1225            var card = storedOutletCard();
1226            if (card) {
1227                window._lpSelectedStore = card;
1228                var cAddrEl = $('#orderTypeModal_address');
1229                if (cAddrEl) cAddrEl.textContent = [card.locality, card.city].filter(
1229Boolean).join(', ');
1230                renderOrderTypes(card);
1231                return;
1232            }
1233            // Nothing known at all (e.g. an outlet picked on the store-locator
1234            // before this record existed). Send logged-in users to their saved
1235            // addresses so they can re-anchor a location with coords; only
1236            // anonymous users go to step 1.
1237            var ud = userData();
1238            showLocationStep(ud.token && ud.contactMappingId ? 'savedAddresses' : 'location');
1239            return;
1240        }
1241
1242        var addrEl = $('#orderTypeModal_address');
1243        if (addrEl) {
1244            // Fall back to stored locality/city (in-memory state is empty after a reload for a
1245            // store-locator pick, which has no userAddress) so this never shows a bare ", ".
1246            var loc  = state.locality || localStorage.getItem('locality') || '';
1247            var cty  = state.city     || localStorage.getItem('city')     || '';
1248            // Prefer the in-memory buffer (not yet committed to localStorage) for display.
1249            var pend = state.pendingAddr && state.pendingAddr.text;
1250            addrEl.textContent = pend || localStorage.getItem('userAddress') || [loc, cty].filter(Boolean).join(', ');
1251        }
1252
1253        // api.uengage.in/client/getNearestOpenOutlet returns a single { address: {...} },
1254        // so wrap into an array for the downstream picker logic.
1255        var url = API_BASE + '/client/getNearestOpenOutlet'
1256                + '?parentBusinessId=' + pId
1257                + '&latitude=' + lat
1258                + '&longitude=' + lng
1259                + (window._lpPrebooking ? '&prebooking=1' : '');
1260        fetch(url, { method: 'GET', headers: apiHeaders(), credentials: 'omit' })
1261            .then(function (r) { return r.json(); })
1262            .then(function (d) {
1263                var rows = [];
1264                if (d && Number(d.status) === 1 && d.address) {
1265                    rows = Array.isArray(d.address) ? d.address : [d.address];
1266                } else if (d && (d.data || d.rows)) {
1267                    rows = d.data || d.rows;
1268                }
1269                if (!rows.length) {
1270                    showLocationStep('notServiceable');
1271                    return;
1272                }
1273                state.nearest = rows;
1274                var store = pickNearestOpen(rows);
1275                window._lpSelectedStore = store;
1276
1277                // Order type already chosen (e.g. home-page Action Center card) — skip the
1278                // visual orderType step and go straight to the redirect, same as if the user
1279                // had tapped that type's button here. Falls through to the normal picker if
1280                // the nearest outlet doesn't actually support the pre-chosen type.
1281                var pendingType = window._lpPendingOrderType;
1282                window._lpPendingOrderType = null;
1283                if (pendingType) {
1284                    var flagKey = FLAG_BY_TYPE[pendingType];
1285                    var supported = flagKey ? Number(store[flagKey]) === 1 : false;
1286                    if (Number(pendingType) === 1) {
1287                        supported = supported && Number(store.onlineDeliveryOn !== undefined ? store.onlineDeliveryOn : 1) === 1;
1288                    }
1289                    if (supported) {
1290                        selectOrderTypeAndRedirect(pendingType);
1291                        return;
1292                    }
1293                }
1294                renderOrderTypes(store);
1295            })
1296            .catch(function () { showLocationStep('notServiceable'); });
1297    }
1298
1299    function pickNearestOpen(stores) {
1300        for (var i = 0; i < stores.length; i++) {
1301            if (stores[i].isOpen === true || Number(stores[i].isOpen) === 1) return stores[i];
1302        }
1303        return stores[0];
1304    }
1305
1306    // The outlet the visitor is currently browsing, recorded by _seedCurrentOutlet
1307    // below. Read back only while it still describes the stored outlet: `business`
1308    // is the authority, and a journey pick overwrites that without touching this.
1309    var OUTLET_CARD_KEY = 'v4_current_outlet';
1310    function storedOutletCard() {
1311        var business = localStorage.getItem('business');
1312        if (!business || business === 'null') return null;
1313        try {
1314            var card = JSON.parse(localStorage.getItem(OUTLET_CARD_KEY) || 'null');
1315            return (card && card.id && String(card.id) === String(business)) ? card : null;
1316        } catch (e) { return null; }
1317    }
1318
1319    var FLAG_BY_TYPE = {
1320        1: 'onlineOrdersDelivery',
1321        2: 'onlineOrdersSelfPickup',
1322        3: 'dineInOrders',
1323        4: 'inCarOrders',
1324        5: 'reservationAllowed',
1325        6: 'uengage_pay'
1326    };
1327
1328    function renderOrderTypes(store) {
1329        var enabledIds = [];
1330        Object.keys(FLAG_BY_TYPE).forEach(function (id) {
1331            var on = Number(store[FLAG_BY_TYPE[id]]) === 1;
1332            if (Number(id) === 1) on = on && Number(store.onlineDeliveryOn !== undefined ? store.onlineDeliveryOn : 1) === 1;
1333            if (on) enabledIds.push(id);
1334            var btn = document.getElementById('orderTypeBtn_' + id);
1335            if (btn) btn.style.display = on ? '' : 'none';
1336        });
1337        var grid = document.getElementById('orderTypeButtonsContainer');
1338        var single = enabledIds.length === 1;
1339        if (grid) grid.classList.toggle('order-type-grid--single', single);
1340
1341        // No order type at all (every flag off): all cards are hidden, so the grid would
1342        // sit empty under a "how would you like your order?" prompt. Swap in a message
1343        // instead — the "Change" control above it is the way out.
1344        var none = enabledIds.length === 0;
1345        var empty = document.getElementById('orderTypeEmpty');
1346        if (empty) empty.hidden = !none;
1347        if (grid) grid.hidden = none;
1348
1349        // With a single order type there is no choice to make, so the "Select an order
1350        // type…" framing reads oddly. Reframe the heading as a continue prompt; the lone
1351        // card is styled as a full-width primary CTA (see .order-type-grid--single CSS).
1352        var heading = document.getElementById('orderTypeHeading');
1353        if (heading) {
1354            // With no order type the highlighted band below states the situation, so the
1355            // prompt is dropped rather than repeated. style.display, not [hidden] — the
1356            // heading's own rule carries !important and would win over the attribute.
1357            heading.style.display = none ? 'none' : '';
1358            if (!none) {
1359                heading.textContent = single
1360                    ? "Great, you're covered! Continue to place your order"
1361                    : "Great, you're covered! How would you like your order?";
1362            }
1363        }
1364
1365        // Heal a stale stored orderType (e.g. delivery='1' left over from a previous
1366        // session on a brand where delivery is now disabled). The header info bar renders
1367        // this value verbatim — without correcting it, it shows "Delivery" for a pickup-only
1368        // outlet. Reset to the first enabled type and refresh the bar so the label matches
1369        // what the outlet actually offers.
1370        var stored = localStorage.getItem('orderType');
1371        if (enabledIds.length && enabledIds.indexOf(String(stored)) === -1) {
1372            localStorage.setItem('orderType', enabledIds[0]);
1373            updateHeaderInfoBar();
1374        }
1375    }
1376
1377    function selectOrderTypeAndRedirect(typeId) {
1378        var store = window._lpSelectedStore;
1379        if (!store && state.nearest.length) store = state.nearest[0];
1380        if (!store) return;
1381
1382        // Delivery is the one type that needs the CUSTOMER's address: it decides
1383        // serviceability and it is where the order goes. The step can now be
1384        // opened on a known outlet alone (changeOrderType above), so this is the
1385        // point where that path collects one instead of committing a delivery
1386        // order with nowhere to deliver it. A pending address counts — it is
1387        // committed by flushPendingAddr() moments later.
1388        // No-op for every pre-existing route here, which only ever arrive with an
1389        // address already in hand.
1390        if (Number(typeId) === 1) {
1391            var addr    = localStorage.getItem('userAddress');
1392            var pending = state.pendingAddr && state.pendingAddr.text;
1393            if (!pending && (!addr || addr === 'null' || addr === '')) {
1394                var udd = userData();
1395                showLocationStep(udd.token && udd.contactMappingId ? 'savedAddresses' : 'location');
1396                return;
1397            }
1398        }
1399        // Real outlet + order type confirmed. Wait for any in-flight add-address save so the
1400        // freshly-created deliveryAddressId is committed (not the previously selected one).
1401        // Bound the wait so a slow/failed save can't block the redirect — flushPendingAddr then
1402        // clears the stale id instead of reusing it.
1403        if (state.savePromise) {
1404            var done = false;
1405            var once = function () { if (done) return; done = true; commitAndRedirect(typeId, store); };
1406            state.savePromise.then(once, once);
1407            setTimeout(once, 2500);
1408        } else {
1409            commitAndRedirect(typeId, store);
1410        }
1411    }
1412
1413    function commitAndRedirect(typeId, store) {
1414        // Commit the held address — this is the only place it reaches the checkout keys.
1415        flushPendingAddr();
1416        state.savePromise = null;
1417        var slug = store.web_slug || store.slug || '';
1418        localStorage.setItem('orderType', String(typeId));
1419        localStorage.setItem('business', String(store.id || ''));
1420        if (slug) localStorage.setItem('slug', slug);
1421        // TTL stamp — 60-min expiry checked at next page load (top of this module).
1422        localStorage.setItem('locationSelectedAt', String(Date.now()));
1423        // CI3 wla_home_20260507.js:2347-2353 — overwrite locality/city with the selected
1424        // outlet's values so the header info bar shows the outlet (not the user's address).
1425        localStorage.setItem('city', store.city || '');
1426        localStorage.setItem('locality', store.locality || '');
1427        document.cookie = 'view_business=' + (store.id || '') + '; path=/; max-age=2592000';
1428
1429        // A serviceable outlet has been committed — every entry path (saved address, new
1430        // address, locality search, GPS) funnels through here, so this is the one place
1431        // that means "user picked where they're ordering from". Bridge, not an import:
1432        // this module's './analytics.js' import is unversioned against a 30-day CDN cache.
1433        try {
1434            window.Analytics?.trackLocationSelected?.({
1435                businessId: store.id || '', locality: store.locality || '',
1436                city: store.city || '', deliveryType: String(typeId),
1437            });
1438        } catch (e) { /* never block the redirect */ }
1439
1440        // Resolve-only mode (opt-in via window._lpResolveOnly): the caller wants the
1441        // outlet resolved + committed but WITHOUT navigating away (e.g. the pre-booking
1442        // detail page, which must stay on the event). Commit is already done above;
1443        // close the modal and notify via an event, then bail before the redirect.
1444        // Default (flag unset) behavior is unchanged for every other page.
1445        if (window._lpResolveOnly) {
1446            window._lpResolveOnly = false;
1447            window._lpForceSelect = false;   // selection made → allow the modal to close
1448            state.savePromise = null;
1449            closeLocationFlow();
1450            document.dispatchEvent(new CustomEvent('lp:outlet-resolved', {
1451                detail: {
1452                    businessId: String(store.id || ''),
1453                    name:       store.name || store.businessName || store.brandName || '',
1454                    slug:       slug,
1455                    orderType:  typeId,
1456                    city:       store.city || '',
1457                    locality:   store.locality || '',
1458                    lat:        localStorage.getItem('userLat') || '',
1459                    lng:        localStorage.getItem('userLng') || '',
1460                },
1461            }));
1462            return;
1463        }
1464
1465        // Populate redirect view
1466        var addrEl   = $('#redirectModal_address');     if (addrEl) addrEl.textContent = localStorage.getItem('userAddress') || '';
1467        var typeEl   = $('#redirectModal_orderType');   if (typeEl) typeEl.textContent = orderTypeLabel(typeId);
1468        var outletEl = $('#redirectModal_outlet');      if (outletEl) outletEl.textContent = (store.locality || '') + ', ' + (store.city || '');
1469
1470        // If a widget-click queued a destination, honor it (CI3 parity). The /online-order
1471        // shortcut becomes the outlet's menu now that we know the slug.
1472        // Reservation (5) → /reservation/{city}/{slug}; Direct Pay (6) → /direct-pay/{slug}
1473        var target = typeId === 5 ? '/reservation/' + encodeURIComponent(store.city || '') + '/' + slug
1474                   : typeId === 6 ? '/direct-pay/' + slug
1475                   : '/order/' + slug;
1476        if (window._lpPendingNav) {
1477            target = window._lpPendingNav;
1478            window._lpPendingNav = null;
1479            if (/^\/online-order(\/|$|\?)/.test(target)) target = '/order/' + slug;
1480        }
1481        // Silver-widget deep link: append section_slug so the menu jumps to the category.
1482        if (window._lpPendingSection) {
1483            target += (target.indexOf('?') > -1 ? '&' : '?') +
1484                      'section_slug=' + encodeURIComponent(window._lpPendingSection);
1485            window._lpPendingSection = null;
1486        }
1487        // Product-page deep link: append item_slug so the menu opens the item popup.
1488        if (window._lpPendingItemSlug) {
1489            target += (target.indexOf('?') > -1 ? '&' : '?') +
1490                      'item_slug=' + encodeURIComponent(window._lpPendingItemSlug);
1491            window._lpPendingItemSlug = null;
1492        }
1493        window._lpRedirectTarget = target;
1494        showLocationStep('redirect');
1495    }
1496
1497    function orderTypeLabel(id) {
1498        // Same canonical set as ORDER_TYPE_LABELS below — this file carried two
1499        // maps that disagreed with each other ("Dine In" here, "Dine In" there,
1500        // "Pickup" vs "Pick Up") as well as with the menu page.
1501        return ({ 1: 'Delivery', 2: 'Pickup', 3: 'Dine-in', 4: 'In-car', 5: 'Reservation', 6: 'Direct Pay' })[id] || '';
1502    }
1503
1504    // Zomato-style auto-redirect: a progress bar fills over REDIRECT_MS, then navigates.
1505    // The cancel button (cancelRedirect) halts it; the "Change" link still works as before.
1506    var REDIRECT_MS = 3000;
1507    function startRedirectCountdown() {
1508        clearInterval(window._lpCountdownIv);
1509        clearTimeout(window._lpRedirectTimeout);
1510        var loader = $('#redirectLoader');
1511        if (loader) loader.classList.remove('redirect-loader--cancelled');
1512        var bar = $('#redirectProgressBar');
1513        if (bar) {
1514            bar.style.transition = 'none';
1515            bar.style.width = '0%';
1516            void bar.offsetWidth;               // force reflow so the next change animates
1517            bar.style.transition = 'width ' + REDIRECT_MS + 'ms linear';
1518            bar.style.width = '100%';
1519        }
1520        // Live 3 → 2 → 1 countdown in the progress text (matches the filling bar).
1521        var textEl = $('#redirectProgressText');
1522        var secs = Math.round(REDIRECT_MS / 1000);
1523        var setText = function (n) { if (textEl) textEl.textContent = 'Taking you to the menu in ' + n + '...'; };
1524        setText(secs);
1525        window._lpCountdownIv = setInterval(function () {
1526            secs -= 1;
1527            if (secs <= 0) { clearInterval(window._lpCountdownIv); return; }
1528            setText(secs);
1529        }, 1000);
1530        window._lpRedirectTimeout = setTimeout(function () {
1531            clearInterval(window._lpCountdownIv);
1532            if (window._lpRedirectTarget) window.location.href = window._lpRedirectTarget;
1533        }, REDIRECT_MS);
1534    }
1535
1536    // Cross beside the progress bar: cancel the auto-redirect and return to the
1537    // order-type step (the previous step) so the user can re-choose.
1538    function cancelRedirect() {
1539        clearTimeout(window._lpRedirectTimeout);
1540        clearInterval(window._lpCountdownIv);
1541        showLocationStep('orderType');
1542    }
1543
1544    function changeUserLocation() {
1545        clearInterval(window._lpCountdownIv);
1546        clearTimeout(window._lpRedirectTimeout);
1547        state.nearest = [];
1548        // CI3 wla_home_20260507.js:1326 — "DON'T clear localStorage here — data is only
1549        // overwritten when user makes a new selection." Wiping business/slug/deliveryAddressId
1550        // here erased the outlet that drives the header info bar, so opening "Change" and
1551        // closing without picking left the bar with nothing to show (it vanished). Keep the
1552        // prior selection; a new pick overwrites it, a cancel preserves it.
1553        var ud = userData();
1554        // Logged-in users came from the saved-addresses list; send them back there
1555        // instead of the initial entry step so they can pick a different address.
1556        showLocationStep(ud.token && ud.contactMappingId ? 'savedAddresses' : 'location');
1557        showOverlay();
1558    }
1559
1560    // ─── "Explore Menu" (notServiceable step) ───────────────
1561    // The user's location has no serviceable (in-radius, open) outlet, so getNearestOpenOutlet
1562    // returned nothing and the dead-end "notServiceable" modal is showing. Instead of re-hitting
1563    // that same radius-bound endpoint (which would return null again), fall back to the radius-free
1564    // getNearestOutlet — it returns the closest outlet regardless of radius/open-hours — and send
1565    // the user to its menu so they can still browse.
1566    // Browse-only: we do NOT flush the held address (the location isn't serviceable, so no
1567    // delivery address is committed to checkout) — only the buffer is cleared.
1568    // Last resort: if even getNearestOutlet has nothing (or the call fails), route to the
1569    // store-locator so the button is never a dead end.
1570    function exploreNearestOutlet() {
1571        clearPendingAddr();
1572        var lat = state.lat || parseFloat(localStorage.getItem('userLat') || '0');
1573        var lng = state.lng || parseFloat(localStorage.getItem('userLng') || '0');
1574
1575        var EXPLORE_OUTLET_URL = API_BASE + '/client/getNearestOutlet'
1576                + '?parentBusinessId=' + pId
1577                + '&latitude=' + lat
1578                + '&longitude=' + lng
1579                + (window._lpPrebooking ? '&prebooking=1' : '');
1580
1581        fetch(EXPLORE_OUTLET_URL, { method: 'GET', headers: apiHeaders(), credentials: 'omit' })
1582            .then(function (r) { return r.json(); })
1583            .then(function (d) {
1584                var store = null;
1585                if (d && Number(d.status) === 1 && d.address) {
1586                    store = Array.isArray(d.address) ? d.address[0] : d.address;
1587                } else if (d && (d.data || d.rows)) {
1588                    var arr = d.data || d.rows;
1589                    store = Array.isArray(arr) ? arr[0] : arr;
1590                }
1591                var slug = store && (store.web_slug || store.slug || '');
1592                if (slug) {
1593                    // Outlet IS selected (the user is browsing this menu), even though no
1594                    // serviceable DELIVERY location was set. Persist the outlet tuple so
1595                    // home/product don't re-prompt — mirrors the store-locator .sl-cta--primary
1596                    // handler below (outlet picked ≠ user location set): store business/slug/
1597                    // city/locality, clear the stale (non-serviceable) user-location keys so
1598                    // "Your Location" reads "No location selected" and changeOrderType() routes
1599                    // to the address step. No address is flushed and no order type is set, so
1600                    // checkout still asks for a real deliverable address.
1601                    localStorage.setItem('slug', slug);
1602                    localStorage.setItem('business', String(store.id || ''));
1603                    if (store.city)     localStorage.setItem('city',     store.city);
1604                    if (store.locality) localStorage.setItem('locality', store.locality);
1605                    localStorage.removeItem('userLat');
1606                    localStorage.removeItem('userLng');
1607                    localStorage.removeItem('userLocSource');
1608                    localStorage.removeItem('userAddress');
1609                    localStorage.removeItem('userAddressLabel');
1610                    localStorage.removeItem('deliveryAddressId');
1611                    localStorage.removeItem('selectedDeliveryAddressId');
1612                    localStorage.setItem('locationSelectedAt', String(Date.now()));
1613                    if (store.id) document.cookie = 'view_business=' + store.id + '; path=/; max-age=2592000';
1614                }
1615                window.location.href = slug ? '/order/' + slug : '/store-locator';
1616            })
1617            .catch(function () { window.location.href = '/store-locator'; });
1618    }
1619
1620    // ─── Map adapters (Google / Leaflet) ────────────────────
1621    function loadLeaflet() {
1622        if (window.L && window.L.map) return Promise.resolve();
1623        return new Promise(function (resolve) {
1624            var link = document.createElement('link');
1625            link.rel = 'stylesheet'; link.href = 'https://unpkg.com/[email protected]/dist/leaflet.css';
1626            document.head.appendChild(link);
1627            var s = document.createElement('script');
1628            s.src = 'https://unpkg.com/[email protected]/dist/leaflet.js';
1629            s.onload = resolve; document.head.appendChild(s);
1630        });
1631    }
1632
1633    function loadGoogleMaps() {
1634        if (window.google && window.google.maps && window.google.maps.Map) return Promise.resolve();
1635        if (window._gmapsPromise) return window._gmapsPromise;
1636        window._gmapsPromise = new Promise(function (resolve) {
1637            window._gmapsReady = resolve;
1638            var s = document.createElement('script');
1639            s.src = 'https://maps.googleapis.com/maps/api/js?key=' + encodeURIComponent(mapsKey) + '&libraries=places&loading=async&callback=_gmapsReady';
1640            document.head.appendChild(s);
1641        });
1642        return window._gmapsPromise;
1643    }
1644
1645    var mapInstance = { kind: null, map: null, marker: null };
1646
1647    // Teardrop pin shared by both map engines (no dependency on Leaflet's default icon
1648    // images). Fixed green for now per design; swap to var(--brand) to theme per brand.
1649    function brandPinSvg() {
1650        var c = '#22c55e';
1651        return '<svg xmlns="http://www.w3.org/2000/svg" width="34" height="44" viewBox="0 0 34 44">'
1652             + '<path d="M17 0C7.6 0 0 7.6 0 17c0 11.9 15.3 25.6 16 26.2.6.5 1.4.5 2 0C18.7 42.6 34 28.9 34 17 34 7.6 26.4 0 17 0z" fill="' + c + '"/>'
1653             + '<circle cx="17" cy="17" r="6" fill="#fff"/></svg>';
1654    }
1655
1656    function initAddressMap() {
1657        // CI3 wla_home_20260507.js:1577-1583 — seed the map center from stored coords so the
1658        // form opens on the user's last-known location, not the Delhi fallback.
1659        var sLat = parseFloat(localStorage.getItem('userLat') || '');
1660        var sLng = parseFloat(localStorage.getItem('userLng') || '');
1661        var hasStored = isFinite(sLat) && isFinite(sLng) && !(sLat === 0 && sLng === 0);
1662        var lat = state.lat || (hasStored ? sLat : 28.6139);
1663        var lng = state.lng || (hasStored ? sLng : 77.2090);
1664        // CI3 wla_home_20260507.js:1634 — hydrate the form from the initial coords on open so
1665        // locality/city/address show immediately. Add flow only (edit pre-fills its own saved
1666        // address); when no coords exist the GPS auto-fetch in openHomeAddressForm handles it.
1667        var hydrateOnInit = !manageMode && (!!state.lat || hasStored);
1668        if (useGoogle) {
1669            var el = document.getElementById('homeAddressMap');
1670            if (!el) return;
1671            if (mapInstance.map && mapInstance.kind === 'google') {
1672                applyMapCenter(lat, lng);
1673                if (hydrateOnInit) hydrateFormFromLatLng(lat, lng);
1674                return;
1675            }
1676            loadGoogleMaps().then(function () {
1677                var map = new google.maps.Map(el, {
1678                    center: { lat: lat, lng: lng }, zoom: 16,
1679                    mapTypeControl: false, streetViewControl: false, fullscreenControl: false,
1680                    // 'greedy' so one-finger drag pans the map on mobile (default 'auto' is
1681                    // cooperative — one finger scrolls the page, blocking the drag-the-map pin UX).
1682                    gestureHandling: 'greedy', clickableIcons: false
1683                });
1684                mapInstance.kind = 'google'; mapInstance.map = map;
1685                // Centre-pin UX: the pin is a static CSS element (.addr-map__pin) fixed at the map
1686                // centre, so the map moves under it (drag-the-map). Replaces the deprecated
1687                // google.maps.Marker. `idle` fires after the map settles; dedupe skips programmatic/
1688                // tile moves; onPinChanged debounces (400ms) so panning fires ONE geocode.
1689                map.addListener('idle', function () {
1690                    var ctr = map.getCenter(); if (!ctr) return;
1691                    var cl = ctr.lat(), cn = ctr.lng();
1692                    if (mapInstance.suppressGeo) return;        // programmatic resize → don't re-geocode
1693                    if (state.lat != null && Math.abs(cl - state.lat) < 1e-5 && Math.abs(cn - state.lng) < 1e-5) return;
1694                    onPinChanged(cl, cn);
1695                });
1696                map.addListener('click', function (e) { map.panTo(e.latLng); });
1697                wireGooglePlaces();
1698                if (hydrateOnInit) hydrateFormFromLatLng(lat, lng);
1699            });
1700        } else {
1701            var el2 = document.getElementById('homeLeafletMap');
1702            if (!el2) return;
1703            if (mapInstance.map && mapInstance.kind === 'leaflet') {
1704                applyMapCenter(lat, lng);
1705                if (hydrateOnInit) hydrateFormFromLatLng(lat, lng);
1706                return;
1707            }
1708            loadLeaflet().then(function () {
1709                el2.innerHTML = '';
1710                var map = L.map(el2, { zoomControl: true }
1710).setView([lat, lng], 16);
1711                L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '&copy; OpenStreetMap' }).addTo(map);
1712                mapInstance.kind = 'leaflet'; mapInstance.map = map;
1713                // Draggable marker (custom divIcon so we don't depend on Leaflet's default
1714                // marker images). Drag to drop, or tap the map to move the pin there.
1715                var pinIcon = L.divIcon({ className: 'lp-leaflet-pin', html: brandPinSvg(), iconSize: [34, 44], iconAnchor: [17, 44] });
1716                var marker = L.marker([lat, lng], { draggable: true, icon: pinIcon }).addTo(map);
1717                mapInstance.marker = marker;
1718                // Centre-pin UX (see Google branch above): any map movement snaps the pin to the
1719                // new centre and geocodes it, keeping pin + fixed coach-mark aligned. onPinChanged
1720                // debounces (400ms) so panning fires ONE geocode; dedupe skips no-op/resize moves.
1721                map.on('moveend', function () {
1722                    var c = map.getCenter();
1723                    marker.setLatLng(c);                        // always keep pin centred
1724                    if (mapInstance.suppressGeo) return;        // programmatic resize → don't re-geocode
1725                    if (state.lat != null && Math.abs(c.lat - state.lat) < 1e-5 && Math.abs(c.lng - state.lng) < 1e-5) return;
1726                    onPinChanged(c.lat, c.lng);
1727                });
1728                map.on('click', function (e) { map.panTo(e.latlng); });
1729                marker.on('dragend', function () { map.panTo(marker.getLatLng()); });
1730                setTimeout(resizeHomeMap, 200);
1731                if (hydrateOnInit) hydrateFormFromLatLng(lat, lng);
1732            });
1733        }
1734    }
1735
1736    function applyMapCenter(lat, lng) {
1737        if (!mapInstance.map) return;
1738        if (mapInstance.kind === 'google') {
1739            mapInstance.map.setCenter({ lat: lat, lng: lng });
1740            if (mapInstance.marker) mapInstance.marker.setPosition({ lat: lat, lng: lng });
1741        } else {
1742            mapInstance.map.setView([lat, lng], 16);
1743            if (mapInstance.marker) mapInstance.marker.setLatLng([lat, lng]);
1744            setTimeout(resizeHomeMap, 100);
1745        }
1746    }
1747
1748    function wireGooglePlaces() {
1749        var inp = document.getElementById('homeAddressSearch');
1750        if (!inp || !window.google || !google.maps || !google.maps.places) return;
1751        var ac = new google.maps.places.Autocomplete(inp, { fields: ['geometry', 'address_components', 'formatted_address'], componentRestrictions: { country: 'IN' } });
1752        ac.addListener('place_changed', function () {
1753            var p = ac.getPlace();
1754            if (!p || !p.geometry) return;
1755            var lat = p.geometry.location.lat();
1756            var lng = p.geometry.location.lng();
1757            persistCoords(lat, lng, 'address');
1758            applyMapCenter(lat, lng);
1759            state.address = p.formatted_address || state.address;
1760            if (state.address) localStorage.setItem('userAddress', state.address);
1761            // CI3 wla_home_20260507.js:1662-1671 — a search pick is equivalent to a pin
1762            // drop. The Place already carries address_components, so hydrate locality/city/
1763            // state/pincode straight from it (no extra geocode) and unlock Save. No add-time
1764            // serviceability gate — that's decided at checkout. Without this the picked place
1765            // recentred the map but Locality/City stayed empty and Save stayed disabled.
1766            state.lat = lat; state.lng = lng;
1767            applyAddressComponents(p.address_components);
1768            applyGeocodeToForm(lat, lng);
1769            // The picked place STAYS in the field. Clearing it left nothing on screen
1770            // saying what was searched for, so a wrong result could not be recognised
1771            // as wrong, let alone edited — the info box shows where the pin landed, not
1772            // what was asked for. Keeping the query is what makes the two comparable.
1773        });
1774    }
1775
1776    var pinDebounce = null;
1777    function onPinChanged(lat, lng) {
1778        clearTimeout(pinDebounce);
1779        // In-memory only — do NOT call persistCoords here. localStorage writes happen on
1780        // explicit user confirmation (GPS pick, locality pick, saved address, Save Address).
1781        // CI3 wla_home_20260507.js:1604-1612 — drag/click funnel into the same form-hydrate
1782        // path as search; Google brands add unconditionally, serviceability waits for checkout.
1783        pinDebounce = setTimeout(function () { hydrateFormFromLatLng(lat, lng); }, 400);
1784    }
1785
1786    // ─── Header location info bar (CI3-aligned) ─────────────
1787    // CANONICAL order-type labels — see the note in headerV2.js. This bar is the
1788    // mobile counterpart of the header's order-type chip, so a different
1789    // spelling here just moves the mismatch to a breakpoint. Kept as a literal
1790    // copy rather than a shared import on purpose (cached /src/js, unversioned
1791    // nested imports).
1792    var ORDER_TYPE_LABELS = { 1: 'Delivery', 2: 'Pickup', 3: 'Dine-in', 4: 'In-car', 5: 'Reservation', 6: 'Direct Pay' };
1793    // The types where the customer goes to the OUTLET rather than the order
1794    // coming to them — so the bar names the outlet, not their saved address.
1795    // Literal copy of headerV2.js's map for the same reason as the labels above.
1796    var AT_OUTLET_TYPES = { 2: 1, 3: 1, 4: 1, 5: 1, 6: 1 };
1797
1798    function updateHeaderInfoBar() {
1799        var bar = document.getElementById('locationInfoBar');
1800        if (!bar) return;
1801
1802        var addr      = localStorage.getItem('userAddress') || '';
1803        var addrLabel = localStorage.getItem('userAddressLabel') || '';
1804        // Defensive: the checkout hydration path (and older builds) could store a raw
1805        // numeric address-type id here ("1" = Home, "2" = Office, …). The bar renders
1806        // this verbatim, so map a bare number to its label — otherwise it shows "1".
1807        if (/^\d+$/.test(addrLabel)) {
1808            var lblNum = parseInt(addrLabel, 10);
1809            addrLabel = (lblNum !== 0 && TYPE_LABELS[lblNum]) ? TYPE_LABELS[lblNum] : '';
1810        }
1811        var locality  = localStorage.getItem('locality');
1812        var city      = localStorage.getItem('city');
1813        var business  = localStorage.getItem('business');
1814        var slug      = localStorage.getItem('slug');
1815        var orderType = localStorage.getItem('orderType');
1816
1817        var has = function (v) { return v && v !== 'null' && v !== 'undefined'; };
1818        var hasLocation = (has(locality) && has(city) && (has(business) || has(slug))) || has(slug);
1819
1820        // v2's bar is the ONLY location control below 992px — the header chip is
1821        // d-none d-lg-flex and a phone row has no space for it. Hiding the bar
1822        // outright when nothing is stored therefore left a FIRST-TIME mobile
1823        // visitor with no way to set a location at all, while desktop still got
1824        // the chip's "Set location". So v2's bar (marked d-lg-none by base.php)
1825        // stays up in a prompt state instead. v1 keeps hiding: its own entry
1826        // points cover that case and its bar is not breakpoint-scoped.
1827        var isV2Bar    = bar.classList.contains('d-lg-none');
1828        var outletSec  = bar.querySelector('.location-info-bar-divider');
1829        var typeSec    = bar.querySelector('.location-info-bar-section-fixed');
1830
1831        if (!hasLocation) {
1832            if (!isV2Bar) { bar.style.display = 'none'; return; }
1833            var promptEl = bar.querySelector('#headerAddress');
1834            if (promptEl) promptEl.textContent = 'Select location';
1835            var promptLbl = bar.querySelector('#headerAddressLabel');
1836            if (promptLbl) promptLbl.style.display = 'none';
1837            // The ORDER TYPE is still reported here. It is stored independently of
1838            // the location — the menu page writes the outlet's default on load
1839            // (menu.js), and the journey auto-commits a sole enabled type — so
1840            // falling straight through left every other page showing "Set order
1841            // type" over a choice that had in fact been made, while the menu page
1842            // a click earlier showed "Pickup".
1843            Array.prototype.forEach.call(bar.querySelectorAll('.headerOrderType'), function (el) {
1844                el.textContent = ORDER_TYPE_LABELS[orderType] || 'Set order type';
1845            });
1846            // The outlet row says nothing before a location exists, and an empty
1847            // "Selected Outlet" row reads as a rendering fault. v1's fixed
1848            // order-type section goes with it: v1 is not being changed here, and
1849            // its bar has its own rows for this.
1850            if (outletSec) outletSec.style.display = 'none';
1851            if (typeSec)   typeSec.style.display   = 'none';
1852            bar.style.display = '';
1853            return;
1854        }
1855        // Restore whatever the prompt state hid above.
1856        if (outletSec) outletSec.style.display = '';
1857        if (typeSec)   typeSec.style.display   = '';
1858
1859        var displayLocality = has(locality) ? locality : (slug || '');
1860        var displayCity     = has(city) ? city : '';
1861        var displayOutlet   = displayLocality + (displayCity ? ', ' + displayCity : '');
1862
1863        var addrEl  = bar.querySelector('#headerAddress');
1864        var lblEl   = bar.querySelector('#headerAddressLabel');
1865        var outEl   = bar.querySelector('#headerOutlet');
1866        var typeEls = bar.querySelectorAll('.headerOrderType');
1867
1868        // v1 has a separate "Selected Outlet" row to fall back on, so an empty
1869        // address there can read "No location selected". v2's bar is one line
1870        // with no such row, so it falls back the way the desktop chip does —
1871        // address, then locality/city, then the outlet slug — and only says
1872        // "Select location" when there is genuinely nothing.
1873        // v2 ONLY. v1 is deliberately left alone: it has a separate "Selected Outlet"
1874        // row, so its "Your Location" honestly reads "No location selected" here and
1875        // never mislabelled the outlet as the user's address — verified against the
1876        // same state. v2 collapsed both into one chip, which is what made the outlet
1877        // fallback wrong. Nothing to fix on v1, and ~2,500 brands run it.
1878        if (addrEl) {
1879            // WHICH place this names depends on the order type, exactly as the
1880            // header chip's does (headerV2.js refreshLocation): delivery comes to
1881            // the customer, so it names their address; pickup, dine-in, in-car and
1882            // reservation happen AT the outlet, so naming their home address there
1883            // is simply false. The bar showed the address for every type, so a
1884            // visitor who chose pickup on the home page read "Set location" — or
1885            // worse, their own street — under a heading that said "Pick up from",
1886            // while the menu page's band (the same chip, reflowed) named the
1887            // outlet. Same words, same source, both widths.
1888            addrEl.textContent = isV2Bar
1889                ? ((AT_OUTLET_TYPES[orderType] ? displayOutlet : addr) || 'Select location')
1890                : (addr || 'No location selected');
1891        }
1892        if (lblEl) {
1893            if (addr && addrLabel) { lblEl.textContent = addrLabel; lblEl.style.display = ''; }
1894            else                   { lblEl.style.display = 'none'; }
1895        }
1896        if (outEl) outEl.textContent = displayOutlet;
1897        // Same prompt as the desktop chip when no type is stored. v2 ONLY: v1's bar
1898        // keeps its 'Delivery' default, since v1 is not being changed here.
1899        Array.prototype.forEach.call(typeEls, function (el) {
1900            el.textContent = ORDER_TYPE_LABELS[orderType] || (isV2Bar ? 'Set order type' : 'Delivery');
1901        });
1902        bar.style.display = '';
1903    }
1904
1905    function changeOrderType() {
1906        // CI3 wla_home_20260507.js:1402-1429. The order-type step is only correct once the user
1907        // has entered their OWN location (userAddress). An outlet picked from the store locator
1908        // sets business/slug but no userAddress, so there's nothing to anchor the nearest-outlet
1909        // lookup — collect the location first: logged-in users see their saved-address listing,
1910        // anonymous users the entry step (CI3 changeOrderType :1410-1413 → changeUserLocation
1911        // :1327-1331). Don't wipe the stored outlet (CI3 :1403-1404) — if the user opens "Change"
1912        // and closes without picking anything, their current selection survives.
1913        var ud         = userData();
1914        var business   = localStorage.getItem('business');
1915        var hasOutlet  = business && business !== 'null';
1916        var addr       = localStorage.getItem('userAddress');
1917        var hasAddress = addr && addr !== 'null' && addr !== '';
1918
1919        // ...or when the outlet itself is known, even without an address: its own
1920        // flags say which types it offers, and for pickup/dine-in/in-car there is
1921        // no address to anchor anything to. Delivery still collects one — see the
1922        // guard in selectOrderTypeAndRedirect().
1923        if (hasOutlet && (hasAddress || storedOutletCard())) {
1924            showLocationStep('orderType');
1925        } else {
1926            showLocationStep(ud.token && ud.contactMappingId ? 'savedAddresses' : 'location');
1927        }
1928        showOverlay();
1929    }
1930
1931    // ─── Public globals (referenced from modal markup) ──────
1932    window.closeLocationFlow            = closeLocationFlow;
1933    window.CurrentAddress               = CurrentAddress;
1934    window.showLocationStep             = showLocationStep;
1935    // showOverlay is needed alongside showLocationStep when external callers
1936    // (e.g. productPage.js "+ Add") want to actually display the modal — without
1937    // it the step is set but the modal stays hidden.
1938    window.showLocationOverlay          = showOverlay;
1939    window.enterAddressSearchLocality   = enterAddressSearchLocality;
1940    window.homeSearchLocality           = homeSearchLocality;
1941    window.openHomeAddressForm          = openHomeAddressForm;
1942    window.openEditAddressForm          = openEditAddressForm;
1943    window.homeAddrGoToCurrentLocation  = homeAddrGoToCurrentLocation;
1944    window.confirmLocationAndShowForm   = confirmLocationAndShowForm;
1945    window.homeChangeOnMap              = homeChangeOnMap;
1946    window.backToLocationMap            = backToLocationMap;
1947    window.closeAddressForm             = closeAddressForm;
1948    window.saveHomeAddress              = saveHomeAddress;
1949    window.selectOrderTypeAndRedirect   = selectOrderTypeAndRedirect;
1950    window.openLoginOrSavedAddresses    = openLoginOrSavedAddresses;
1951    window.changeUserLocation           = changeUserLocation;
1952    window.exploreNearestOutlet         = exploreNearestOutlet;
1953    window.cancelRedirect               = cancelRedirect;
1954    window.changeOrderType              = changeOrderType;
1955
1956    // ─── Widget click gate ──────────────────────────────────
1957    // Intercept clicks on anchors targeting outlet-dependent paths. If the user has no outlet
1958    // (slug + business), open the modal and queue the destination for after order-type pick.
1959    // If the user has an outlet, /online-order shortcuts straight to /order/{slug} (CI3 parity,
1960    // wla_home_20260507.js:638-700). Other gated hrefs proceed normally.
1961    // Product pages (/products, /product-detail/...) are intentionally NOT gated —
1962    // users should browse the catalogue freely; the location modal is triggered by
1963    // productPage.js when they click "+ Add" on an item (the action that actually
1964    // needs an outlet).
1965    // `/online-order` is intentionally NOT gated — it's the store-locator page where
1966    // anonymous users browse outlets. Picking an outlet there sets localStorage via the
1967    // .sl-cta--primary handler below.
1968    // Single-outlet brands: seed the one outlet so the header has something to
1969    // show. Nothing else does it any more — the disabled-flow branch that used to
1970    // is no longer taken now that these brands run the journey, and arriving on
1971    // /order/{slug} directly writes no location at all. The visible symptom was a
1972    // visitor going straight to the menu, coming back to home, and finding the
1973    // chip on "Set location" for a brand with exactly one outlet.
1974    //
1975    // The two keys are seeded on different rules, because they answer to different
1976    // owners:
1977    //
1978    //   slug — always overwritten, as the config-0 branch above does and for the
1979    //     same reason: with one outlet the only correct slug is this one, so a
1980    //     value left over from a RENAMED outlet is a stale cache, not a choice.
1981    //     Leaving it strands the visitor on /order/{unknown-slug}, which 302s back
1982    //     to home and reads as "the category click just reloads the page". Safe to
1983    //     stamp even after a journey pick — that journey can only have resolved to
1984    //     this same single outlet.
1985    //
1986    //   city/locality — only when unset. These are the VISITOR's, written by the
1987    //     journey from the address they chose. Re-stamping the outlet's over the
1988    //     top on the next page load would quietly replace where they are
1989    //     delivering to with where the shop is.
1990    (function _seedSoleOutlet() {
1991        if (Number(AppConfig.storesCount) !== 1) return;
1992
1993        var d = window.storesDetails;
1994        if (!d || !d.length) return;
1995        var store = d[0];
1996        var slug  = store.web_slug || store.slug || '';
1997        if (!slug) return;
1998
1999        localStorage.setItem('slug', slug);
2000
2001        var haveCity     = localStorage.getItem('city');
2002        var haveLocality = localStorage.getItem('locality');
2003        if (store.city     && (!haveCity     || haveCity     === 'null')) localStorage.setItem('city',     store.city);
2004        if (store.locality && (!haveLocality || haveLocality === 'null')) localStorage.setItem('locality', store.locality);
2005
2006        if (typeof window.hdr2RefreshHeader === 'function') window.hdr2RefreshHeader();
2007    }());
2008
2009    // ─── Adopt the outlet whose menu we are on ──────────────
2010    // A visitor who lands on /order/{slug} from a shared link, a search result or
2011    // a QR code never runs the location journey, so nothing is stored. The MENU
2012    // page still showed the outlet, because the header chip falls back to
2013    // window.OutletConfig (headerV2.js) — but every other page has no such
2014    // fallback, so the same header went from "Pick up from · DLF Midtown Plaza"
2015    // to "Set location" on the next click, as if the visit had been forgotten.
2016    //
2017    // The page is the server's own statement of which outlet this is, so record
2018    // it, exactly as checkout.js does from CheckoutConfig and as the single-outlet
2019    // seeder above does from storesDetails. Same two rules as that seeder:
2020    //
2021    // All four keys are stamped, not merged. commitAndRedirect() above already
2022    // overwrites city/locality with the committed OUTLET's values on every
2023    // journey (CI3 wla_home_20260507.js:2347-2353) precisely so the header names
2024    // the outlet — the visitor's own address lives in userAddress, which nothing
2025    // here touches. Merging instead would leave a visitor who committed outlet A
2026    // and then opened outlet B's menu reading "Pick up from <A's locality>" over
2027    // B's menu. Being on an outlet's page is the same assertion as picking it.
2028    (function _seedCurrentOutlet() {
2029        // v2 headers only. The seeding would be just as correct under v1, but v1
2030        // runs on every live brand and slug/business are state the cart and
2031        // checkout read — that is not a change to make on the whole fleet to fix
2032        // a mismatch reported in the v2 header. Drop this line to widen it.
2033        if (!document.querySelector('.site-header.hdr2')) return;
2034
2035        var oc = window.OutletConfig;
2036        if (!oc || !oc.slug) return;
2037
2038        localStorage.setItem('slug', String(oc.slug));
2039        if (oc.businessId) localStorage.setItem('business', String(oc.businessId));
2040
2041        if (oc.city)     localStorage.setItem('city',     String(oc.city));
2042        if (oc.locality) localStorage.setItem('locality', String(oc.locality));
2043
2044        // Also keep the outlet as a STORE RECORD, in the shape renderOrderTypes()
2045        // and commitAndRedirect() already take. Without it the order-type step is
2046        // unreachable off the menu page: it is built around "find the outlet
2047        // nearest these coordinates", and a visitor who arrived on /order/{slug}
2048        // from a link has no coordinates, so loadOrderTypes() bounced them to
2049        // address entry — while the menu page a click earlier switched order type
2050        // instantly. Same outlet, same choice, two different journeys.
2051        //
2052        // OutletConfig.orderTypes already carries the four flags, so this costs no
2053        // request and no view change. Keyed by business id and checked on read, so
2054        // a later journey pick (which writes a different `business`) retires it
2055        // rather than answering for the wrong outlet.
2056        var ot = oc.orderTypes || {};
2057        try {
2058            localStorage.setItem(OUTLET_CARD_KEY, JSON.stringify({
2059                id: String(oc.businessId || ''),
2060                slug: String(oc.slug || ''),
2061                city: String(oc.city || ''),
2062                locality: String(oc.locality || ''),
2063                onlineOrdersDelivery:   Number(ot.delivery) === 1 ? 1 : 0,
2064                onlineDeliveryOn:       Number(ot.delivery) === 1 ? 1 : 0,
2065                onlineOrdersSelfPickup: Number(ot.pickup)   === 1 ? 1 : 0,
2066                dineInOrders:           Number(ot.dine_in)  === 1 ? 1 : 0,
2067                inCarOrders:            Number(ot.in_car)   === 1 ? 1 : 0,
2068            }));
2069        } catch (e) { /* storage full or blocked — the step just falls back */ }
2070
2071        updateHeaderInfoBar();
2072        if (typeof window.hdr2RefreshHeader === 'function') window.hdr2RefreshHeader();
2073    }());
2074
2075    var GATED_HREF_RE = /^\/(order\/|cart|checkout)(\/|$|\?)/;
2076
2077    document.addEventListener('click', function (e) {
2078        if (e.defaultPrevented) return;
2079        if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
2080        var a = e.target.closest('a[href]');
2081        if (!a) return;
2082        if (a.target === '_blank') return;
2083        // Store-locator cards are the explicit outlet-pick surface — never gate them.
2084        if (a.closest('.sl-card')) return;
2085        var href = a.getAttribute('href') || '';
2086        if (!GATED_HREF_RE.test(href)) return;
2087        // One outlet → nothing for this gate to resolve. A /order/ link already
2088        // names the only outlet there is, so stopping it to ask "where?" gates a
2089        // choice the brand has already made. Those brands keep the journey on the
2090        // header controls; every other route in runs unimpeded. /cart and
2091        // /checkout follow the same reasoning — they act on a cart that can only
2092        // belong to that one outlet.
2093        //
2094        // This also preserves how those brands behaved before they were given the
2095        // journey: their config was forced to 0, so this module returned early and
2096        // the gate was never installed for them at all.
2097        if (Number(AppConfig.storesCount) === 1) return;
2098
2099        var slug     = localStorage.getItem('slug');
2100        var business = localStorage.getItem('business');
2101        var hasOutletPick = slug && slug !== 'null' && business && business !== 'null';
2102
2103        if (hasOutletPick) return; // gated paths proceed naturally when outlet is set
2104
2105        e.preventDefault();
2106        window._lpPendingNav = href;
2107        var ud = userData();
2108        showLocationStep(ud.token && ud.contactMappingId ? 'savedAddresses' : 'location');
2109        showOverlay();
2110    }, true);
2111
2112    // Store-locator outlet-pick — when user clicks the primary "Order Online" CTA
2113    // OR taps anywhere on the card (whole-card click in storeLocator.js fires a
2114    // synthetic click on the primary link, which bubbles here), persist the outlet
2115    // to localStorage so the home info bar shows the selection. Let the link
2116    // navigate normally afterwards (to /order/{slug}).
2117    document.addEventListener('click', function (e) {
2118        var btn = e.target.closest('.sl-cta--primary');
2119        if (!btn) return;
2120        var card = btn.closest('.sl-card');
2121        if (!card) return;
2122        var d = card.dataset || {};
2123        var cardSlug     = d.slug || '';
2124        var cardBusiness = d.business || '';
2125        if (!cardSlug || !cardBusiness) return;
2126        // Display-friendly locality (with original casing) when available, lowercase fallback.
2127        var locality = d.localityDisplay || d.locality || '';
2128        var city     = d.city || '';
2129        localStorage.setItem('slug',      cardSlug);
2130        localStorage.setItem('business',  cardBusiness);
2131        if (locality) localStorage.setItem('locality', locality);
2132        if (city)     localStorage.setItem('city',     city);
2133        // Picking an outlet is NOT the user entering their own delivery location (CI3
2134        // wla_home_20260507.js:3111-3114 stores only business/slug/city/locality — no coords,
2135        // no userAddress). So clear any stale user address (and the saved-address id it came
2136        // from): "Your Location" must read "No location selected" (CI3 header.php:564), and
2137        // changeOrderType() must route to the address step — not the order-type step — until
2138        // the user actually sets their location.
2139        localStorage.removeItem('userLat');
2140        localStorage.removeItem('userLng');
2141        localStorage.removeItem('userLocSource');
2142        localStorage.removeItem('userAddress');
2143        localStorage.removeItem('userAddressLabel');
2144        localStorage.removeItem('deliveryAddressId');
2145        localStorage.removeItem('selectedDeliveryAddressId');
2146        localStorage.setItem('locationSelectedAt', String(Date.now()));
2147        document.cookie = 'view_business=' + cardBusiness + '; path=/; max-age=2592000';
2148        updateHeaderInfoBar();
2149    }, true);
2150
2151    // Silver-theme widget handlers — `silverCatClick(section, outletSlug, noOfStores)` is
2152    // inlined by widget partials and already includes its own location gate, but it doesn't
2153    // queue a post-pick destination; `silverItemClick(section, itemSlug, bId)` is referenced
2154    // in markup but NOT defined inline (broken in the legacy template), so the +Add buttons
2155    // on home item cards silently fail. We override both with versions that:
2156    //   • If location set → navigate to /order/{userOutlet}?section_slug&item_slug
2157    //   • If no location → queue section + item, open the modal; redirect happens after
2158    //     order-type selection (selectOrderTypeAndRedirect consumes _lpPendingSection/Item).
2159    // One outlet → a tile has nothing to disambiguate, so it goes straight to that
2160    // outlet's menu instead of opening the journey. Those brands DO run the journey
2161    // now (base.php stopped forcing their config to 0), but it belongs to the header
2162    // controls alone: "where do I deliver to" and "which order type" are worth asking
2163    // on demand, while a category tile asking them first is a gate in front of a
2164    // choice that was already made by the brand having a single store.
2165    //
2166    // Slug sources in order: the one the widget passed, the home page's
2167    // storesDetails, then localStorage. Returns '' when none resolves, and the
2168    // callers fall through to the normal gate rather than navigating to /order/.
2169    function _soleOutletSlug(passed) {
2170        if (Number(AppConfig.storesCount) !== 1) return '';
2171        if (passed && passed !== 'null') return String(passed);
2172        var d = window.storesDetails;
2173        if (d && d.length) {
2174            var slug = d[0].web_slug || d[0].slug || '';
2175            if (slug) return String(slug);
2176        }
2177        var stored = localStorage.getItem('slug');
2178        return (stored && stored !== 'null') ? String(stored) : '';
2179    }
2180
2181    function _gatedSilverCatClick(sectionSlug, outletSlug, noOfStores) {
2182        var sole = _soleOutletSlug(outletSlug);
2183        if (sole) {
2184            var soleUrl = '/order/' + sole;
2185            if (sectionSlug) soleUrl += '?section_slug=' + encodeURIComponent(sectionSlug);
2186            window.location.href = soleUrl;
2187            return;
2188        }
2189        var s  = localStorage.getItem('slug');
2190        var b  = localStorage.getItem('business');
2191        var ok = s && s !== 'null' && b && b !== 'null';
2192        if (!ok) {
2193            if (sectionSlug) window._lpPendingSection = sectionSlug;
2194            var u = userData();
2195            showLocationStep(u.token && u.contactMappingId ? 'savedAddresses' : 'location');
2196            showOverlay();
2197            return;
2198        }
2199        var target = (Number(noOfStores) === 1 && outletSlug && outletSlug !== 'null') ? outletSlug : s;
2200        var url = '/order/' + target;
2201        if (sectionSlug) url += '?section_slug=' + encodeURIComponent(sectionSlug);
2202        window.location.href = url;
2203    }
2204
2205    function _gatedSilverItemClick(sectionSlug, itemSlug, businessId) {
2206        // Third arg is a businessId, not a slug, so nothing to pass through here —
2207        // _soleOutletSlug falls back to storesDetails (see home.php).
2208        var sole = _soleOutletSlug('');
2209        if (sole) {
2210            var soleQs = [];
2211            if (sectionSlug) soleQs.push('section_slug=' + encodeURIComponent(sectionSlug));
2212            if (itemSlug)    soleQs.push('item_slug=' + encodeURIComponent(itemSlug));
2213            window.location.href = '/order/' + sole + (soleQs.length ? '?' + soleQs.join('&') : '');
2214            return;
2215        }
2216        var s  = localStorage.getItem('slug');
2217        var b  = localStorage.getItem('business');
2218        var ok = s && s !== 'null' && b && b !== 'null';
2219        if (!ok) {
2220            if (sectionSlug) window._lpPendingSection  = sectionSlug;
2221            if (itemSlug)    window._lpPendingItemSlug = itemSlug;
2222            var u = userData();
2223            showLocationStep(u.token && u.contactMappingId ? 'savedAddresses' : 'location');
2224            showOverlay();
2225            return;
2226        }
2227        var url = '/order/' + s;
2228        var qs  = [];
2229        if (sectionSlug) qs.push('section_slug=' + encodeURIComponent(sectionSlug));
2230        if (itemSlug)    qs.push('item_slug=' + encodeURIComponent(itemSlug));
2231        if (qs.length) url += '?' + qs.join('&');
2232        window.location.href = url;
2233    }
2234
2235    function _installSilverWidgetHandlers() {
2236        window.silverCatClick  = _gatedSilverCatClick;
2237        window.silverItemClick = _gatedSilverItemClick;
2238    }
2239    // Replace BEFORE DOMContentLoaded so inline definitions later in the page can't override
2240    // ours — and again after, in case they do (script load order varies). Both stages are
2241    // idempotent because we install the same reference.
2242    _installSilverWidgetHandlers();
2243    if (document.readyState === 'loading') {
2244        document.addEventListener('DOMContentLoaded', _installSilverWidgetHandlers);
2245    }
2246    window.addEventListener('load', _installSilverWidgetHandlers);
2247
2248    // Home widget menu-item / ADD-button gate — these cards render as <article> without
2249    // an href, so the URL gate above misses them. On the home page (only) intercept clicks
2250    // on .add-ctrl__btn / .wla-best__btn and .item / .wla-best__card card bodies so they
2251    // participate in the location flow (open modal if no outlet, else jump to /order/{slug}
2252    // with the item preselected — never /product-detail from the home rails).
2253    // On /products and /product-detail/* this is handled by productPage.js — skip there.
2254    document.addEventListener('click', function (e) {
2255        if (e.defaultPrevented) return;
2256        if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
2257        // Active only on the home route; other pages have their own +Add wiring.
2258        var path = window.location.pathname || '/';
2259        if (path !== '/' && path !== '') return;
2260
2261        // v2 widgets' "Shop the full menu" link — same gate, no item: outlet known
2262        // -> /order/{slug}; otherwise queue and open the journey. Handled here (not
2263        // in the widget) because this module owns location-gated routing.
2264        var menuLink = e.target.closest('[data-w2-menu-link]');
2265        if (menuLink) {
2266            e.preventDefault();
2267            _gatedSilverCatClick('', '', 0);
2268            return;
2269        }
2270
2271        var addBtn   = e.target.closest('.add-ctrl__btn, .wla-best__btn');
2272        var itemCard = addBtn ? null : e.target.closest('.item, .wla-best__card');
2273        if (!addBtn && !itemCard) return;
2274
2275        // If the card itself is wrapped in <a>, the URL gate already covered it.
2276        var card = addBtn ? (addBtn.closest('.item') || addBtn.closest('.wla-best__card')) : itemCard;
2277        if (!card || card.tagName === 'A') return;
2278
2279        var itemSlug = card.getAttribute('data-item-slug') || '';
2280        var slug     = localStorage.getItem('slug');
2281        var business = localStorage.getItem('business');
2282        var hasOutletPick = slug && slug !== 'null' && business && business !== 'null';
2283
2284        e.preventDefault();
2285        if (hasOutletPick) {
2286            // Outlet known — jump straight to the menu (with the item preselected if we have a slug).
2287            var qs = itemSlug ? '?item_slug=' + encodeURIComponent(itemSlug) : '';
2288            window.location.href = '/order/' + slug + qs;
2289            return;
2290        }
2291
2292        // No outlet yet — queue a sensible destination, then open the location flow.
2293        // Catalog URL words are brand-configurable (default products / product-detail).
2294        var _cfg = window.AppConfig || {};
2295        var _listSlug = _cfg.catalogSlug || 'products';
2296        var _detailSlug = _cfg.catalogDetailSlug || 'product-detail';
2297        window._lpPendingNav = itemSlug ? '/' + _detailSlug + '/' + itemSlug : '/' + _listSlug;
2298        var ud2 = userData();
2299        showLocationStep(ud2.token && ud2.contactMappingId ? 'savedAddresses' : 'location');
2300        showOverlay();
2301    }, true);
2302
2303    updateHeaderInfoBar();
2304
2305    // Publish the real header height as --site-header-h so the location bar's
2306    // sticky `top` docks flush under it. Keeps the offset correct regardless of
2307    // logo size / nav padding (no magic numbers); re-synced on resize + font load.
2308    (function syncHeaderHeight() {
2309        function sync() {
2310            var h = document.querySelector('.site-header');
2311            if (!h) return;
2312            var px = Math.round(h.getBoundingClientRect().height);
2313            if (px > 0) document.documentElement.style.setProperty('--site-header-h', px + 'px');
2314        }
2315        sync();
2316        window.addEventListener('resize', sync, { passive: true });
2317        window.addEventListener('load', sync, { passive: true });
2318    })();
2319
2320    // Sticky scroll behavior — add `location-bar-scrolled` class on scroll, remove at top.
2321    (function attachStickyScroll() {
2322        var bar = document.getElementById('locationInfoBar');
2323        if (!bar) return;
2324        var ticking = false;
2325        function onScroll() {
2326            if (ticking) return;
2327            ticking = true;
2328            window.requestAnimationFrame(function () {
2329                var y = window.pageYOffset || document.documentElement.scrollTop || 0;
2330                bar.classList.toggle('location-bar-scrolled', y > 8);
2331                ticking = false;
2332            });
2333        }
2334        window.addEventListener('scroll', onScroll, { passive: true });
2335        onScroll();
2336    })();
2337
2338    // Keep the bar in sync after the flow runs. The visibility/contents are otherwise
2339    // computed only once on load, so a selection made inside the modal (or in another tab)
2340    // wouldn't reflect until a full reload — and a cancelled "Change" could leave it stale.
2341    modalEl.addEventListener('hidden.bs.modal', updateHeaderInfoBar);
2342    // menu.js dispatches this when the order type changes in the SAME tab (its strip,
2343    // or the outlet default it adopts on load), which fires no `storage` event. The bar
2344    // carries its own .headerOrderType copy, so without this the mobile bar went stale
2345    // while the desktop chip updated. Subscribed HERE rather than widening headerV2's
2346    // selector: this function owns the bar's copy, and two writers for one element is
2347    // how they drift apart.
2348    document.addEventListener('ordertype:changed', updateHeaderInfoBar);
2349    window.addEventListener('storage', function (e) {
2350        if (!e.key || _LP_LOCATION_KEYS.indexOf(e.key) !== -1) updateHeaderInfoBar();
2351    });
2352
2353    // ─── Auto-open on first visit ───────────────────────────
2354    // CI3 wla_home_20260507.js:2483-2512 — modal is suppressed only when the user has a
2355    // confirmed outlet tuple (slug + business). userLat/userLng alone don't count because
2356    // they're written before order-type selection completes. If a user does GPS but never
2357    // picks an order type, slug/business stay empty and the modal must reopen.
2358    function maybeAutoOpen() {
2359        if (!AppConfig.autoOpenLocationModal) return;
2360        var slug     = localStorage.getItem('slug');
2361        var business = localStorage.getItem('business');
2362        var hasOutlet = slug && slug !== 'null' && business && business !== 'null';
2363        if (hasOutlet) return;
2364        // CI3 parity (wla_20260417.js:2134): don't stack the location modal on top of the
2365        // first-login welcome form. This Bootstrap modal's focus-trap would steal keystrokes
2366        // from the welcome form's inputs, leaving it visible but un-typeable. firstLoginProfile.js
2367        // runs just before this script, so the overlay is already shown when name is empty.
2368        // The user can reopen location selection from the header pill after finishing the form.
2369        var pp = document.getElementById('profilePromptModal');
2370        if (pp && !pp.hasAttribute('hidden')) return;
2371        var ud = userData();
2372        showLocationStep(ud.token ? 'savedAddresses' : 'location');
2373        showOverlay();
2374    }
2375
2376    if (document.readyState === 'loading') {
2377        document.addEventListener('DOMContentLoaded', maybeAutoOpen);
2378    } else {
2379        maybeAutoOpen();
2380    }
2381})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.