PageSourceSearch

https://voigt-marbella.com/wp-content/plugins/creative-agency-prop…voigt-ai-property-browser.js?ver=1790848883

js voigt-marbella.com collected 2026-10-02 00:02:28 UTC 124,515 bytes, 2,744 lines download raw bytes

1(function($){
2  'use strict';
3
4  // Cloudflare Turnstile auto-render callbacks. They dispatch DOM events so
5  // the active advisor instance can safely enable, pause or retry submission.
6  window.voigtAiBrowserTurnstileReady = function(token) {
7    try { document.dispatchEvent(new CustomEvent('voigt:ai:turnstile-ready', {detail:{token:String(token || '')}})); } catch (e) {}
8  };
9  window.voigtAiBrowserTurnstileExpired = function() {
10    try { document.dispatchEvent(new CustomEvent('voigt:ai:turnstile-expired')); } catch (e) {}
11  };
12  window.voigtAiBrowserTurnstileError = function(code) {
13    try { document.dispatchEvent(new CustomEvent('voigt:ai:turnstile-error', {detail:{code:String(code || '')}})); } catch (e) {}
14  };
15
16  var cfg = window.voigtAiBrowserConfig || {};
17  if (!cfg.ajaxUrl) return;
18
19  function num(value) {
20    var parsed = parseFloat(String(value || '').replace(/[^0-9.-]/g, ''));
21    return isFinite(parsed) ? parsed : null;
22  }
23
24  function uniq(values) {
25    return Array.from(new Set((values || []).filter(Boolean)));
26  }
27
28  function isMobile() {
29    return window.matchMedia('(max-width: 767px)').matches;
30  }
31
32  $(function(){
33    var $root = $('.voigt-ai-browser').first();
34    if (!$root.length) return;
35
36    var $panel = $root.find('.voigt-ai-browser-panel').first();
37    var $backdrop = $root.find('.voigt-ai-browser-backdrop').first();
38    var $messages = $panel.find('.voigt-ai-browser-messages').first();
39    var $form = $();
40    var $listingHost = $();
41    var strings = cfg.strings || {};
42    var pendingSearch = false;
43    var pendingSearchId = '';
44    var pendingSearchTimer = null;
45    var pendingSearchContext = {silent: false, scrollToResults: false};
46    var loadingNode = null;
47    var loadingTimer = null;
48    var loadingSequence = [];
49    var loadingSequenceIndex = 0;
50    var savedScrollY = null;
51    var pendingCriteria = null;
52    var mobilePageLocked = false;
53    var bodyLockSnapshot = null;
54    var hiddenCompetingUi = [];
55    var turnstilePendingAction = null;
56    var turnstileRetryTimer = null;
57    var turnstilePollTimer = null;
58    var turnstileRefreshTimer = null;
59    var turnstileIssuedAt = 0;
60    var turnstileResetAttempts = 0;
61    var turnstileConsumed = false;
62    var $quotaNotice = $panel.find('.voigt-ai-browser-quota').first();
63    var $accountButton = $panel.find('.voigt-ai-browser-account').first();
64    var $memberStatus = $panel.find('[data-member-status]').first();
65    var $authModal = $('.voigt-ai-auth-modal').first();
66    var authBusy = false;
67    var authNonce = String(cfg.authNonce || '');
68    var authNonceFetchedAt = 0;
69    var authNonceRequest = null;
70    var quotaBlocked = false;
71    var currentQuota = cfg.initialQuota && typeof cfg.initialQuota === 'object' ? cfg.initialQuota : {};
72
73    // The language of the page being viewed is authoritative for the Advisor UI.
74    // TranslatePress changes the URL (e.g. /pl/), so a conversation opened on an
75    // English page must never overwrite the Polish UI after the visitor switches
76    // language. Keep an independent chat session per page language instead.
77    function detectPageLanguage() {
78      var path = String(window.location.pathname || '');
79      var match = path.match(/^\/(pl|en|es|de|fr)(?:\/|$)/i);
80      if (match) return String(match[1] || 'en').toLowerCase();
81      var htmlLang = String(document.documentElement.getAttribute('lang') || '').toLowerCase().slice(0, 2);
82      if (['pl','en','es','de','fr'].indexOf(htmlLang) !== -1) return htmlLang;
83      var configured = String($root.attr('data-lang') || cfg.lang || 'en').toLowerCase().slice(0, 2);
84      return ['pl','en','es','de','fr'].indexOf(configured) !== -1 ? configured : 'en';
85    }
86
87    var pageLanguage = detectPageLanguage();
88    $root.attr('data-lang', pageLanguage);
89
90    var storageKey = 'voigtAiBrowserSession:v6:' + pageLanguage;
91    var legacyGlobalKey = 'voigtAiBrowserSession:v5';
92    var legacyKey = 'voigtAiBrowserSession:' + location.pathname;
93    var stored = {};
94    try {
95      stored = JSON.parse(sessionStorage.getItem(storageKey) || '{}') || {};
96      // Migrate the old global session only when it belongs to this exact page
97      // language. Never revive an English session on /pl/ (or vice versa).
98      if (!stored.id && !stored.token) {
99        var legacyStored = JSON.parse(sessionStorage.getItem(legacyGlobalKey) || sessionStorage.getItem(legacyKey) || '{}') || {};
100        var legacyLang = String(legacyStored.lang || '').toLowerCase().slice(0, 2);
101        if (legacyStored && (legacyStored.id || legacyStored.token) && legacyLang === pageLanguage) {
102          stored = legacyStored;
103          sessionStorage.setItem(storageKey, JSON.stringify(stored));
104        }
105      }
106    } catch (e) {}
107    var sessionId = stored.id || '';
108    var sessionToken = stored.token || '';
109    var turnstileSessionVerified = stored.turnstileVerified === true;
110    var trackedPropertyOpens = {};
111    var conversationKey = 'voigtAiBrowserConversation:v3:' + pageLanguage;
112    var legacyConversationKey = 'voigtAiBrowserConversation:v2';
113    var conversationHistory = [];
114    var currentSuggestions = [];
115    var restoredConversation = false;
116    var unreadStorageKey = 'voigtAiBrowserUnread:v2:' + pageLanguage;
117    // The teaser is a one-time discovery hint for the whole site, not a
118    // per-page/per-language prompt. Persist both exposure and an explicit
119    // dismissal so navigating to another property or returning later does not
120    // make the bubble reappear.
121    var teaserSeenKey = 'voigtAiTeaserSeen:v2';
122    var teaserDismissedKey = 'voigtAiTeaserDismissed:v2';
123    var unreadCount = 0;
124    try { unreadCount = Math.max(0, parseInt(sessionStorage.getItem(unreadStorageKey) || '0', 10) || 0); } catch (e) {}
125
126    function unreadLabel(count) {
127      var lang = currentLanguage();
128      if (lang === 'pl') return count === 1 ? '1 nowa wiadomość od AI' : count + ' nowe wiadomości od AI';
129      if (lang === 'es') return count === 1 ? '1 mensaje nuevo de IA' : count + ' mensajes nuevos de IA';
130      if (lang === 'de') return count === 1 ? '1 neue KI-Nachricht' : count + ' neue KI-Nachrichten';
131      return count === 1 ? '1 new AI message' : count + ' new AI messages';
132    }
133
134    function renderUnreadBadge() {
135      $('.voigt-ai-browser-launcher').each(function(){
136        var $launcher = $(this);
137        var $badge = $launcher.find('.voigt-ai-browser-unread').first();
138        if (!$badge.length) {
139          $badge = $('<span class="voigt-ai-browser-unread" aria-l
139ive="polite" aria-atomic="true" hidden>0</span>').appendTo($launcher);
140        }
141        var visible = unreadCount > 0;
142        $badge.text(unreadCount > 9 ? '9+' : String(unreadCount)).prop('hidden', !visible);
143        $launcher.toggleClass('has-unread', visible);
144        if (visible) {
145          $badge.attr('aria-label', unreadLabel(unreadCount));
146          $launcher.attr('data-unread-label', unreadLabel(unreadCount));
147        } else {
148          $badge.removeAttr('aria-label');
149          $launcher.removeAttr('data-unread-label');
150        }
151      });
152    }
153
154    function setUnreadCount(value) {
155      unreadCount = Math.max(0, parseInt(value, 10) || 0);
156      try { sessionStorage.setItem(unreadStorageKey, String(unreadCount)); } catch (e) {}
157      renderUnreadBadge();
158    }
159
160    function incrementUnreadCount() {
161      setUnreadCount(unreadCount + 1);
162    }
163
164    function panelIsOpen() {
165      return $panel.hasClass('is-open');
166    }
167
168    function readOpenPropertyContext() {
169      var node = document.getElementById('voigt-ai-open-property-context');
170      if (!node) return {};
171      try {
172        var data = JSON.parse(node.textContent || node.innerText || '{}');
173        if (!data || typeof data !== 'object' || data.page_type !== 'property_detail') return {};
174        return data;
175      } catch (e) { return {}; }
176    }
177
178    var openPropertyContext = readOpenPropertyContext();
179
180    function hasOpenPropertyContext() {
181      return !!(openPropertyContext && (openPropertyContext.reference || openPropertyContext.title));
182    }
183
184    function propertyContextPayload() {
185      if (!hasOpenPropertyContext()) return '';
186      openPropertyContext.page_url = String(openPropertyContext.page_url || window.location.href || '');
187      try { return JSON.stringify(openPropertyContext); } catch (e) { return ''; }
188    }
189
190    function trackAdvisorEvent(eventName, reference) {
191      if (!sessionId || !sessionToken || !eventName) return;
192      var ref = String(reference || '').trim().toUpperCase();
193      var key = String(eventName) + ':' + ref;
194      if (trackedPropertyOpens[key]) return;
195      trackedPropertyOpens[key] = true;
196      $.post(cfg.ajaxUrl, {
197        action: 'voigt_ai_browser_event',
198        nonce: cfg.nonce,
199        session_id: sessionId,
200        session_token: sessionToken,
201        event: String(eventName),
202        reference: ref
203      });
204    }
205
206    function initialGreeting() {
207      if (!hasOpenPropertyContext()) return String(strings.greeting || '');
208      var ref = String(openPropertyContext.reference || '').trim();
209      var title = String(openPropertyContext.title || '').trim();
210      var label = title || ref;
211      if (label.length > 120) label = label.slice(0, 117) + '…';
212      var lang = currentLanguage();
213      if (lang === 'pl') return 'Widzę, że oglądasz ' + label + (ref && title ? ' (' + ref + ')' : '') + '. Możesz zapytać mnie bezpośrednio o tę nieruchomość — nie musisz podawać numeru referencyjnego.';
214      if (lang === 'es') return 'Veo que está viendo ' + label + (ref && title ? ' (' + ref + ')' : '') + '. Puede preguntarme directamente por esta propiedad sin indicar la referencia.';
215      if (lang === 'de') return 'Ich sehe, dass Sie ' + label + (ref && title ? ' (' + ref + ')' : '') + ' ansehen. Sie können direkt nach dieser Immobilie fragen, ohne die Referenz zu nennen.';
216      return 'I can see that you are viewing ' + label + (ref && title ? ' (' + ref + ')' : '') + '. You can ask me directly about this property without giving the reference.';
217    }
218
219    function currentLanguage() {
220      // UI/session language follows the page, not the language remembered by a
221      // previous TranslatePress page or by an earlier AI response.
222      return pageLanguage;
223    }
224
225    function nativeUiFallback(en, de, pl) {
226      var lang = currentLanguage();
227      if (lang === 'de') return de;
228      if (lang === 'pl') return pl || en;
229      return en;
230    }
231
232    function isAgentPanelContext() {
233      return !!document.querySelector('#vcs-agent-panel');
234    }
235
236    function authenticatedAgentToken() {
237      // The backend validates the token, so the browser should not suppress it
238      // based on CSS classes or tab timing. Those guards caused valid Agent
239      // Panel requests to be silently treated as public traffic.
240      if (!isAgentPanelContext()) return '';
241      try { return localStorage.getItem('vcsAgentToken') || ''; } catch (e) { return ''; }
242    }
243
244    // Portal every floating UI element directly under <body>. This avoids
245    // stacking contexts created by Elementor, sticky headers and mobile menus.
246    $panel.addClass('voigt-ai-browser-panel-portal').appendTo(document.body);
247    $backdrop.addClass('voigt-ai-browser-backdrop-portal').appendTo(document.body);
248    if ($authModal.length) $authModal.appendTo(document.body);
249    $('.voigt-ai-browser-launcher, .voigt-ai-browser-teaser').appendTo(document.body);
250    if (isAgentPanelContext()) $panel.find('.voigt-ai-browser-account').prop('hidden', true).hide();
251    renderUnreadBadge();
252
253    function tapTapCandidates() {
254      var selectors = [
255        '[id*="taptap" i]', '[class*="taptap" i]',
256        '[id*="tap-tap" i]', '[class*="tap-tap" i]',
257        '[id*="tap_menu" i]', '[class*="tap_menu" i]',
258        '[id*="tapmenu" i]', '[class*="tapmenu" i]',
259        '[id*="bonfire" i]', '[class*="bonfire" i]'
260      ];
261      try {
262        return Array.prototype.slice.call(document.querySelectorAll(selectors.join(',')));
263      } catch (e) {
264        // Fallback for older browsers without case-insensitive attribute selectors.
265        return Array.prototype.slice.call(document.querySelectorAll('body *')).filter(function(element){
266          var signature = ((element.id || '') + ' ' + (element.className || '') + ' ' + (element.getAttribute('aria-label') || '')).toLowerCase();
267          return /taptap|tap-tap|tap_menu|tapmenu|bonfire/.test(signature);
268        });
269      }
270    }
271
272    function hideCompetingMobileUi() {
273      if (!isMobile()) return;
274      restoreCompetingMobileUi();
275      var seen = [];
276      tapTapCandidates().forEach(function(candidate){
277        if (!candidate || candidate === document.body || candidate === document.documentElement) return;
278        if (candidate.closest('.voigt-ai-browser-panel-portal')) return;
279
280        // TapTap often places the identifying class on a child while the fixed
281        // stacking context lives on an ancestor. Hide that fixed/sticky root.
282        var element = candidate;
283        var node = candidate;
284        while (node && node !== document.body) {
285          var nodeStyle = window.getComputedStyle(node);
286          if (nodeStyle.position === 'fixed' || nodeStyle.position === 'sticky') {
287            element = node;
288            break;
289          }
290          node = node.parentElement;
291        }
292        if (seen.indexOf(element) !== -1) return;
293        seen.push(element);
294
295        hiddenCompetingUi.push({
296          element: element,
297          opacity: element.style.opacity,
298          pointerEvents: element.style.pointerEvents,
299          visibility: element.style.visibility,
300          transition: element.style.transition,
301          ariaHidden: element.getAttribute('aria-hidden')
302        });
303        element.style.transition = 'opacity .18s ease';
304        element.style.opacity = '0';
305        element.style.pointerEvents = 'none';
306        element.style.visibility = 'hidden';
307        element.setAttribute('aria-hidden', 'true');
308        element.setAttribute('data-voigt-ai-temporarily-hidden', '1');
309      });
310    }
311
312    function restoreCompetingMobileUi() {
313      hiddenCompetingUi.forEach(function(entry){
314        var element = entry.element;
315        if (!element || !document.documentElement.contains(element)) return;
316        element.style.opacity = entry.opacity;
317        element.style.pointerEvents = entry.pointerEvents;
318        element.style.visibility = entry.visibility;
319        element.style.transition = entry.transition;
320        if (entry.ariaHidden === null) element.removeAttribute('aria-hidden');
321        else element.setAttribute('aria-hidden', entry.ariaHidden);
322        element.removeAttribute('data-voigt-ai-temporarily-hidden');
323      });
324      hiddenCompetingUi = [];
325    }
326
327    function resetListingBinding() {
328      $form = $();
329      $listingHost = $();
330    }
331
332    function getListingHost(force) {
333      if (force) resetListingBinding();
334      if ($listingHost.length && document.documentElement.contains($listingHost.get(0))) return $listingHost;
335
336      // The advisor is portalled to body. Bind it to the native Property List
337      // that contains a real search form, prioritising the authenticated panel.
338      var $candidates = $('#vcs-agent-panel .voigt-ai-browser-listing-host[data-agent-mode="1"], .voigt-ai-browser-listing-host');
339      var best = null;
340      var bestScore = -1;
341      $candidates.each(function(){
342        var $candidate = $(this);
343        var $candidateForm = $candidate.find('form.mls-proplist-search-form').first();
344        if (!$candidateForm.length) return;
345        var score = 0;
346        if ($candidate.closest('#vcs-agent-panel').length) score += 100;
347        if ($candidate.is(':visible')) score += 30;
348        if ($candidateForm.find('#mls_search_features_search_type').length) score += 20;
349        if ($candidateForm.find('input.mls-af-checkbox').length) score += 20;
350        if ($candidate.find('#mls-ajax-results').length) score += 10;
351        if (score > bestScore) { bestScore = score; best = this; }
352      });
353      if (best) $listingHost = $(best);
354      return $listingHost;
355    }
356
357    function getForm(force) {
358      if (force) resetListingBinding();
359      if ($form.length && document.documentElement.contains($form.get(0))) return $form;
360
361      var $host = getListingHost();
362      var $forms = $host.length ? $host.find('form.mls-proplist-search-form') : $('form.mls-proplist-search-form');
363      var best = null;
364      var bestScore = -1;
365      $forms.each(function(){
366        var $candidate = $(this);
367        var score = 0;
368        if ($candidate.closest('#vcs-agent-panel').length) score += 100;
369        if ($candidate.is(':visible')) score += 30;
370        if ($candidate.find('#mls_search_features_search_type').length) score 
370+= 30;
371        if ($candidate.find('input.mls-af-checkbox').length) score += 30;
372        if ($candidate.find('#mls_search_type').length) score += 10;
373        if (score > bestScore) { bestScore = score; best = this; }
374      });
375      if (best) {
376        $form = $(best);
377        if (!$listingHost.length) $listingHost = $form.closest('.voigt-ai-browser-listing-host');
378      }
379      return $form;
380    }
381
382    function getFilterScope(force) {
383      var $currentForm = getForm(force);
384      if ($currentForm.length) return $currentForm;
385      var $host = getListingHost(force);
386      return $host.length ? $host : $currentForm;
387    }
388
389    function normaliseFeatureId(value) {
390      var text = String(value || '').trim();
391      try { text = decodeURIComponent(text); } catch (e) {}
392      return fold(text).replace(/[^a-z0-9]/g, '');
393    }
394
395    function normaliseFeatureWords(value) {
396      return fold(value).replace(/[^a-z0-9]+/g, ' ').replace(/\s+/g, ' ').trim();
397    }
398
399    function getFeatureInputs(force) {
400      var $currentForm = getForm(force);
401      var $inputs = $currentForm.find('input.mls-af-checkbox');
402      if (!$inputs.length) {
403        var $scope = getListingHost();
404        $inputs = $scope.find('form.mls-proplist-search-form input.mls-af-checkbox');
405      }
406      return $inputs;
407    }
408
409    function featureLookup(param) {
410      var lookup = cfg.featureLookup || {};
411      if (lookup[param]) return lookup[param];
412      var wanted = normaliseFeatureId(param);
413      var found = null;
414      Object.keys(lookup).some(function(key){
415        if (normaliseFeatureId(key) === wanted) {
416          found = lookup[key];
417          return true;
418        }
419        return false;
420      });
421      return found || {};
422    }
423
424    function findFeatureInputs(params, force) {
425      var exact = new Set((params || []).map(function(value){ return String(value); }));
426      var normalised = new Set((params || []).map(normaliseFeatureId).filter(Boolean));
427      var labels = new Set();
428      (params || []).forEach(function(param){
429        var meta = featureLookup(param);
430        if (meta && meta.name) labels.add(normaliseFeatureWords(meta.name));
431      });
432      return getFeatureInputs(force).filter(function(){
433        var $input = $(this);
434        var value = String(this.value || '');
435        var label = normaliseFeatureWords($input.next('label').text());
436        return exact.has(value) || normalised.has(normaliseFeatureId(value)) || (label && labels.has(label));
437      });
438    }
439
440    function featureLabel(param) {
441      var $input = findFeatureInputs([param]).first();
442      if ($input.length) {
443        var text = $.trim($input.next('label').text());
444        if (text) return text;
445      }
446      var meta = featureLookup(param);
447      return String(meta.name || param || '');
448    }
449
450    function setFeatureChecked($input, checked) {
451      $input.each(function(){
452        var input = this;
453        var desired = !!checked;
454        // A native click follows exactly the same event path as a user click and
455        // lets main.js create/remove the visible badges. Fall back to explicit
456        // state + bubbling events if a browser refuses a click on a hidden panel.
457        if (!!input.checked !== desired) {
458          try { input.click(); } catch (e) {}
459        }
460        if (!!input.checked !== desired) {
461          input.checked = desired;
462          try { input.dispatchEvent(new Event('input', {bubbles: true})); } catch (e) { $(input).trigger('input'); }
463          try { input.dispatchEvent(new Event('change', {bubbles: true})); } catch (e) { $(input).trigger('change'); }
464        }
465        if (desired) input.setAttribute('checked', 'checked');
466        else input.removeAttribute('checked');
467        $(input).closest('li').toggleClass('mls-selected', desired).attr('aria-selected', desired ? 'true' : 'false');
468      });
469    }
470
471    function rebuildFeatureUi() {
472      var $scope = getFilterScope();
473      $scope.find('.mls-af-sel-wrap').each(function(){
474        var $wrap = $(this);
475        var $badges = $wrap.find('.mls-af-selected-labels').first().empty();
476        $wrap.find('input.mls-af-checkbox:checked').each(function(){
477          var $input = $(this);
478          var text = $.trim($input.next('label').text());
479          if (!text) return;
480          $('<span class="mls-af-label-badge" data-voigt-ai-badge="1">')
481            .attr('data-value', String(this.value))
482            .append(document.createTextNode(text))
483            .append($('<span class="mls-af-close-btn">&times;</span>').attr('aria-label', nativeUiFallback('Remove', 'Entfernen', 'Usuń')))
484            .appendTo($badges);
485        });
486      });
487
488      var $selectedFeatures = $scope.find('.mls_selected_features').first();
489      if (!$selectedFeatures.length) $selectedFeatures = getForm().closest('.filter-form-wrapper').find('.mls_selected_features').first();
490      $selectedFeatures.empty();
491      getFeatureInputs().filter(':checked').each(function(){
492        var text = $.trim($(this).next('label').text());
493        if (text) $('<span>').text(text).appendTo($selectedFeatures);
494      });
495    }
496
497    function featureCategoryField(category) {
498      var key = fold(category).replace(/[^a-z0-9]/g, '');
499      var map = {
500        setting:'Setting', orientation:'Orientation', condition:'Condition', pool:'Pool',
501        climate:'Climate', climatecontrol:'Climate', views:'Views', features:'Features',
502        furniture:'Furniture', kitchen:'Kitchen', garden:'Garden', security:'Security',
503        parking:'Parking', utilities:'Utilities', category:'Category', rentals:'Rentals',
504        plots:'Plots', plotsandventures:'Plots'
505      };
506      return map[key] || '';
507    }
508
509    function ensureFeatureSubmissionMirrors(requestedParams) {
510      var $currentForm = getForm();
511      if (!$currentForm.length) return;
512      $currentForm.find('input.voigt-ai-feature-mirror').remove();
513
514      (requestedParams || []).forEach(function(param){
515        var $input = findFeatureInputs([param]).first();
516        if ($input.length && $currentForm.has($input).length) return;
517
518        var name = $input.attr('name') || '';
519        if (!name) {
520          var meta = featureLookup(param);
521          var field = featureCategoryField(meta.category || '');
522          if (field) name = 'mls_search_feature_' + field + '[]';
523        }
524        if (!name) return;
525        $('<input type="hidden" class="voigt-ai-feature-mirror">')
526          .attr('name', name)
527          .val(String($input.length ? $input.val() : param))
528          .appendTo($currentForm);
529      });
530    }
531
532    function applyMoreFilters(criteria, force) {
533      criteria = criteria || {};
534      var hardFeatures = uniq(criteria.must_have_features || []);
535      var preferredFeatures = uniq(criteria.preferred_features || []);
536      var appliedFeatures = hardFeatures.length ? hardFeatures : preferredFeatures;
537      var $currentForm = getForm(!!force);
538      var $all = getFeatureInputs();
539
540      // Never clear checkboxes from another Property List on the same page.
541      setFeatureChecked($all.filter(':checked'), false);
542      var $matched = findFeatureInputs(appliedFeatures);
543      setFeatureChecked($matched, true);
544
545      var $mode = $currentForm.find('#mls_search_features_search_type').first();
546      $mode.val(hardFeatures.length ? '2' : (preferredFeatures.length ? '1' : '2'));
547      nativeChange($mode);
548
549      rebuildFeatureUi();
550      ensureFeatureSubmissionMirrors(appliedFeatures);
551
552      var appliedIds = [];
553      appliedFeatures.forEach(function(param){
554        if (findFeatureInputs([param]).filter(':checked').length) appliedIds.push(param);
555      });
556      window.__voigtAiFeatureSync = {
557        formFound: !!$currentForm.length,
558        availableCheckboxes: $all.length,
559        requested: appliedFeatures.slice(),
560        applied: uniq(appliedIds),
561        unresolved: appliedFeatures.filter(function(param){ return !findFeatureInputs([param]).filter(':checked').length; }),
562        mode: String($mode.val() || '')
563      };
564      try { document.dispatchEvent(new CustomEvent('voigtAiMoreFiltersApplied', {detail: window.__voigtAiFeatureSync})); } catch (e) {}
565      return appliedFeatures;
566    }
567
568    function applyMoreFiltersWhenReady(criteria, callback) {
569      var requested = uniq([].concat(criteria.must_have_features || [], criteria.preferred_features || []));
570      var attempts = 0;
571      var maxAttempts = 40; // up to four seconds for delayed/page-builder markup
572
573      function attempt() {
574        attempts++;
575        getForm(true);
576        var available = getFeatureInputs().length;
577        if (!requested.length || available > 0 || attempts >= maxAttempts) {
578          var applied = applyMoreFilters(criteria, false);
579          // Give delegated change handlers one animation frame to update their UI
580          // before serialising and submitting the native form.
581          window.requestAnimationFrame(function(){
582            window.setTimeout(function(){ callback(applied); }, 20);
583          });
584          return;
585        }
586        window.setTimeout(attempt, 100);
587      }
588      attempt();
589    }
590
591    function getResultsRoot() {
592      var $host = getListingHost();
593      var $results = $host.find('#mls-ajax-results').first();
594      return $results.length ? $results : $('#mls-ajax-results').first();
595    }
596
597    function turnstileRequired() {
598      return !!cfg.turnstileEnabled && !isAgentPanelContext() && !turnstileSessionVerified;
599    }
600
601    function tsToken() {
602      var input = $panel.find('input[name="cf-turnstile-response"]').get(0);
603      return input ? String(input.value || '') : '';
604    }
605
606    function setTurnstileStatus(message, isError) {
607      var $status = $panel.find('.voigt-ai-browser-security-status').first();
608      if (!$status.length) return;
609      if (!message || !turnstileRequired()) {
610        $status.attr('hidden', true).removeClass('is-error').text('');
611        return;
612      }
613      $status.text(message).toggleClass('is-error', !!isError).removeAttr('hidden');
614    }
615
616    function quotaTemplate(template, first, second) {
617      var firstValue = String(Math.max(0, parseInt(first, 10) || 0));
618      var secondValue = String(Math.max(0, parseInt(second, 10) || 0));
619      return String(template || '')
620        .replace(/%1\$d/g, firstValue)
621        .replace(/%2\$d/g, secondValue)
622        .replace(/%d/g, firstValue);
623    }
624
625    function accountBenefits($container, memberLimit, guestLimit) {
626      if (!$container || !$container.length) return;
627      var $benefits = $('<div class="voigt-ai-browser-quota-benefits">');
628      $('<strong>
628').text(strings.account_benefits_title || nativeUiFallback('Free account benefits', 'Vorteile eines kostenlosen Kontos', 'Korzyści z bezpłatnego konta')).appendTo($benefits);
629      var $list = $('<ul>').appendTo($benefits);
630      [
631        quotaTemplate(strings.auth_benefit_limit, memberLimit, guestLimit),
632        strings.auth_benefit_searches || '',
633        strings.auth_benefit_favorites || '',
634        strings.auth_benefit_price_alerts || ''
635      ].filter(Boolean).forEach(function(item){ $('<li>').text(item).appendTo($list); });
636      $benefits.appendTo($container);
637    }
638
639    function updateQuotaNotice(quota) {
640      quota = quota && typeof quota === 'object' ? quota : {};
641      currentQuota = $.extend({}, currentQuota || {}, quota);
642      var tier = String(currentQuota.tier || (cfg.loggedInUser ? 'member' : 'guest'));
643      var reason = String(currentQuota.reason || '');
644      var remaining = parseInt(currentQuota.remaining, 10);
645      var limit = parseInt(currentQuota.limit, 10);
646      var threshold = parseInt(currentQuota.warning_threshold, 10);
647      if (!isFinite(remaining)) remaining = 0;
648      if (!isFinite(limit)) limit = 0;
649      if (!isFinite(threshold) || threshold < 1) threshold = 5;
650      var guestDailyLimit = Math.max(1, parseInt(cfg.guestDailyLimit, 10) || 30);
651      var memberDailyLimit = Math.max(guestDailyLimit, parseInt(cfg.memberDailyLimit, 10) || 100);
652
653      if ($accountButton.length && tier === 'guest') {
654        $accountButton.text(strings.limit_login || nativeUiFallback('Sign in', 'Anmelden', 'Zaloguj się'));
655        $accountButton.attr('title', strings.limit_login_more || '');
656      }
657      if ($memberStatus.length && tier === 'member') {
658        $memberStatus.text(strings.member_status || nativeUiFallback('Signed in', 'Angemeldet', 'Zalogowano'));
659      }
660
661      if (isAgentPanelContext() || tier === 'agent') {
662        quotaBlocked = false;
663        if ($quotaNotice.length) $quotaNotice.attr('hidden', true).removeClass('is-warning is-blocked is-member is-rate');
664        syncTurnstileControls();
665        return;
666      }
667
668      var isRate = reason === 'rate';
669      var exhausted = !isRate && limit > 0 && remaining <= 0;
670      var shouldWarn = !exhausted && !isRate && limit > 0 && remaining <= threshold;
671      quotaBlocked = exhausted;
672
673      if (!$quotaNotice.length || (!isRate && !exhausted && !shouldWarn)) {
674        if ($quotaNotice.length) {
675          $quotaNotice.attr('hidden', true).removeClass('is-warning is-blocked is-member is-rate');
676          $quotaNotice.find('.voigt-ai-browser-quota-text').text('');
677          $quotaNotice.find('.voigt-ai-browser-quota-actions').empty();
678        }
679        syncTurnstileControls();
680        return;
681      }
682
683      var text = '';
684      if (isRate) {
685        text = strings.rate_limited || strings.error || '';
686      } else if (exhausted) {
687        text = tier === 'member'
688          ? (strings.member_limit_exhausted || strings.error || '')
689          : (strings.limit_exhausted || strings.error || '');
690      } else {
691        text = tier === 'member'
692          ? (strings.member_messages_left || '')
693          : (strings.messages_left || '');
694        if (tier === 'guest' && strings.limit_login_more) text += ' ' + strings.limit_login_more;
695      }
696
697      $quotaNotice
698        .removeAttr('hidden')
699        .toggleClass('is-warning', shouldWarn)
700        .toggleClass('is-blocked', exhausted)
701        .toggleClass('is-member', tier === 'member')
702        .toggleClass('is-rate', isRate);
703      $quotaNotice.find('.voigt-ai-browser-quota-text').text(text);
704
705      var $actions = $quotaNotice.find('.voigt-ai-browser-quota-actions').empty();
706      if (tier === 'guest' && (shouldWarn || exhausted)) {
707        accountBenefits($actions, memberDailyLimit, guestDailyLimit);
708        var loginUrl = String(currentQuota.login_url || cfg.loginUrl || '');
709        var registerUrl = String(currentQuota.register_url || cfg.registerUrl || '');
710        if (loginUrl) {
711          var $loginAction = cfg.authPopupEnabled && $authModal.length
712            ? $('<button type="button" class="voigt-ai-browser-quota-login" data-auth-open="login">')
713            : $('<a class="voigt-ai-browser-quota-login">').attr('href', loginUrl);
714          $loginAction.text(strings.limit_login || nativeUiFallback('Sign in', 'Anmelden', 'Zaloguj się')).appendTo($actions);
715        }
716        if (registerUrl || (cfg.registrationEnabled && cfg.authPopupEnabled && $authModal.length)) {
717          var $registerAction = cfg.authPopupEnabled && $authModal.length
718            ? $('<button type="button" class="voigt-ai-browser-quota-register" data-auth-open="register">')
719            : $('<a class="voigt-ai-browser-quota-register">').attr('href', registerUrl);
720          $registerAction.text(strings.limit_register || nativeUiFallback('Create account', 'Konto erstellen', 'Utwórz konto')).appendTo($actions);
721        }
722      }
723      syncTurnstileControls();
724    }
725
726    function syncTurnstileControls() {
727      var required = turnstileRequired();
728      var waiting = required && (turnstileConsumed || !tsToken());
729      var userWaiting = waiting && !!turnstilePendingAction;
730      $panel.find('.voigt-ai-browser-turnstile').toggle(required).attr('aria-hidden', required ? 'false' : 'true');
731      // Pre-warm Turnstile silently. The composer stays usable while the token
732      // is being prepared; if the visitor submits early, ensureTurnstileReady()
733      // queues that action and sends it as soon as verification completes.
734      $panel.find('.voigt-ai-browser-composer button[type="submit"]').prop('disabled', quotaBlocked);
735      $panel.find('.voigt-ai-browser-composer textarea').prop('disabled', quotaBlocked);
736      var $lead = $panel.find('.voigt-ai-browser-send-lead');
737      if (!$lead.data('voigtLeadSent')) $lead.prop('disabled', false);
738      if (userWaiting) setTurnstileStatus(strings.verifying || nativeUiFallback('Completing a security check…', 'Sicherheitsprüfung wird abgeschlossen…', 'Kończenie weryfikacji bezpieczeństwa…'), false);
739      else setTurnstileStatus('', false);
740    }
741
742    function runPendingTurnstileAction() {
743      if (!turnstilePendingAction || !tsToken() || turnstileConsumed) return;
744      var action = turnstilePendingAction;
745      turnstilePendingAction = null;
746      window.setTimeout(function(){ action(); }, 0);
747    }
748
749    function clearTurnstileRefreshTimer() {
750      if (turnstileRefreshTimer) window.clearTimeout(turnstileRefreshTimer);
751      turnstileRefreshTimer = null;
752    }
753
754    function scheduleTurnstileRefreshNearExpiry() {
755      clearTurnstileRefreshTimer();
756      if (!turnstileRequired() || !tsToken() || turnstileConsumed) return;
757      // Cloudflare tokens expire after 300 seconds. Refresh an unused pre-warmed
758      // token shortly before that point, rather than after every chat message.
759      var elapsed = turnstileIssuedAt ? Math.max(0, Date.now() - turnstileIssuedAt) : 0;
760      var delay = Math.max(1000, 270000 - elapsed);
761      turnstileRefreshTimer = window.setTimeout(function(){
762        turnstileRefreshTimer = null;
763        if (!turnstileRequired() || !tsToken() || turnstileConsumed) return;
764        turnstileConsumed = true;
765        try {
766          if (window.turnstile) {
767            var element = $panel.find('.voigt-ai-browser-turnstile').get(0);
768            if (element) {
769              window.turnstile.reset(element);
770              return;
771            }
772          }
773        } catch (e) {}
774        scheduleTurnstileReset(250, false);
775      }, delay);
776    }
777
778    function scheduleTurnstileReset(delay, retryMessage) {
779      if (!turnstileRequired()) return;
780      if (turnstileRetryTimer) window.clearTimeout(turnstileRetryTimer);
781      turnstileRetryTimer = window.setTimeout(function(){
782        turnstileRetryTimer = null;
783        if (turnstilePendingAction) {
784          setTurnstileStatus(retryMessage ? (strings.verification_retry || strings.verifying || nativeUiFallback('Retrying the security check…', 'Sicherheitsprüfung wird erneut versucht…', 'Ponawianie weryfikacji bezpieczeństwa…')) : (strings.verifying || nativeUiFallback('Completing a security check…', 'Sicherheitsprüfung wird abgeschlossen…', 'Kończenie weryfikacji bezpieczeństwa…')), false);
785        } else {
786          setTurnstileStatus('', false);
787        }
788        try {
789          if (window.turnstile) {
790            var element = $panel.find('.voigt-ai-browser-turnstile').get(0);
791            if (element) {
792              window.turnstile.reset(element);
793              turnstileResetAttempts = 0;
794              return;
795            }
796          }
797        } catch (e) {}
798        turnstileResetAttempts++;
799        if (!tsToken() && turnstileResetAttempts < 20) scheduleTurnstileReset(500, true);
800      }, Math.max(0, delay || 0));
801    }
802
803    function tsReset() {
804      if (!turnstileRequired()) return;
805      turnstileConsumed = true;
806      syncTurnstileControls();
807      scheduleTurnstileReset(50, false);
808    }
809
810    function ensureTurnstileReady(action) {
811      if (!turnstileRequired()) return true;
812      if (tsToken() && !turnstileConsumed) return true;
813      turnstilePendingAction = action;
814      syncTurnstileControls();
815      // Handles delayed script loading and widgets that entered an error state.
816      scheduleTurnstileReset(250, false);
817      return false;
818    }
819
820    function initialiseTurnstileGate() {
821      if (!cfg.turnstileEnabled) return;
822      if (isAgentPanelContext()) {
823        $panel.find('.voigt-ai-browser-turnstile,.voigt-ai-browser-security-status').attr('hidden', true).hide();
824        syncTurnstileControls();
825        return;
826      }
827      turnstileConsumed = !tsToken();
828      syncTurnstileControls();
829      // Fallback for cached/optimised pages where auto-render callbacks can be
830      // registered after the widget has already populated its hidden input.
831      turnstilePollTimer = window.setInterval(function(){
832        if (!turnstileRequired()) return;
833        var token = tsToken();
834        if (token && turnstileConsumed) {
835          turnstileConsumed = false;
836          syncTurnstileControls();
837          runPendingTurnstileAction();
838        } else if (!token && !turnstileConsumed) {
839          turnstileConsumed = true;
840          syncTurnstileControls();
841        }
842      }, 250);
843    }
844
845    document.addEventListener('voigt:ai:turnstile-ready', function(){
846      if (!turnstileRequired()) return;
847      turnstileConsumed = false;
848      turnstileIssuedAt = Date.now();
849      turnstileResetAttempts = 0;
850      syncTurnstileControls();
851      scheduleTurnstileRefreshNearExpiry();
852      runPendingTurnstileAction();
853    });
854    document.addEventListener('voigt:ai:turnstile-expired', function(){
855      if (!turnstileRequired()) return;
856      clearTurnstileRefreshTimer();
857      turnstileIssuedAt = 0;
858      turnstileConsumed = true;
859      syncTurnstileControls();
860      scheduleTurnstileReset(100, true);
861    });
862    document.addEventListener('voigt:ai:turnstile-error', function(){
863      if (!turnstileRequired()) return;
864      clearTurnstileRefreshTimer();
865      turnstileIssuedAt = 0;
866      turnstileConsumed = true;
867      syncTurnstileControls();
868      scheduleTurnstileReset(1200, true);
869    });
870
871    function saveSession() {
872      stored = {id: sessionId, token: sessionToken, lang: pageLanguage, turnstileVerified: turnstileSessionVerified === true};
873      try {
874        sessionStorage.setItem(storageKey, JSON.stringify(stored));
875      } catch (e) {}
876    }
877
878    function serialisableOptions(options) {
879      options = options || {};
880      var navigationCriteria = {};
881      if (options.navigationCriteria && typeof options.navigationCriteria === 'object') {
882        try {
883          navigationCriteria = JSON.parse(JSON.stringify(options.navigationCriteria));
884        } catch (e) {
885          navigationCriteria = {};
886        }
887      }
888      return {
889        small: options.small ? String(options.small) : '',
890        cta: options.cta ? String(options.cta) : '',
891        href: options.href ? String(options.href) : '',
892        messageId: options.messageId ? String(options.messageId) : '',
893        feedback: options.feedback && typeof options.feedback === 'object' ? options.feedback : {},
894        navigationCriteria: navigationCriteria
895      };
896    }
897
898    function saveConversation() {
899      try {
900        sessionStorage.setItem(conversationKey, JSON.stringify({
901          version: 2,
902          session_id: sessionId || '',
903          language: currentLanguage(),
904          messages: conversationHistory.slice(-40),
905          suggestions: currentSuggestions.slice(0, 12),
906          saved_at: Date.now()
907        }));
908      } catch (e) {}
909    }
910
911    function clearConversationStorage() {
912      conversationHistory = [];
913      currentSuggestions = [];
914      try { sessionStorage.removeItem(conversationKey); } catch (e) {}
915    }
916
917    function clearSession() {
918      sessionId = '';
919      sessionToken = '';
920      turnstileSessionVerified = false;
921      clearTurnstileRefreshTimer();
922      turnstileIssuedAt = 0;
923      stored = {};
924      window.__voigtAiLastCriteria = {};
925      clearConversationStorage();
926      try {
927        sessionStorage.removeItem(storageKey);
928        sessionStorage.removeItem(legacyKey);
929        sessionStorage.removeItem('voigtAiPendingCriteria');
930        sessionStorage.removeItem('voigtAiPanelClosedForNavigation');
931      } catch (e) {}
932      setUnreadCount(0);
933    }
934
935    function lockMobilePage() {
936      if (!isMobile() || mobilePageLocked) return;
937      savedScrollY = window.scrollY || window.pageYOffset || 0;
938      bodyLockSnapshot = {
939        position: document.body.style.position,
940        top: document.body.style.top,
941        left: document.body.style.left,
942        right: document.body.style.right,
943        width: document.body.style.width,
944        overflow: document.body.style.overflow
945      };
946      document.body.style.position = 'fixed';
947      document.body.style.top = (-savedScrollY) + 'px';
948      document.body.style.left = '0';
949      document.body.style.right = '0';
950      document.body.style.width = '100%';
951      document.body.style.overflow = 'hidden';
952      mobilePageLocked = true;
953    }
954
955    function unlockMobilePage() {
956      if (!mobilePageLocked) return;
957      var y = savedScrollY || 0;
958      var snap = bodyLockSnapshot || {};
959      document.body.style.position = snap.position || '';
960      document.body.style.top = snap.top || '';
961      document.body.style.left = snap.left || '';
962      document.body.style.right = snap.right || '';
963      document.body.style.width = snap.width || '';
964      document.body.style.overflow = snap.overflow || '';
965      mobilePageLocked = false;
966      bodyLockSnapshot = null;
967      savedScrollY = null;
968      window.scrollTo(0, y);
969    }
970
971    function applyVisualViewport() {
972      if (!isMobile()) {
973        $panel.css({'--vai-vh':'', '--vai-vtop':'', '--vai-vleft':'', '--vai-vwidth':''}).removeClass('is-keyboard-open');
974        return;
975      }
976      var vv = window.visualViewport;
977      var height = vv ? vv.height : window.innerHeight;
978      var top = vv ? vv.offsetTop : 0;
979      var left = vv ? vv.offsetLeft : 0;
980      var width = vv ? vv.width : window.innerWidth;
981      $panel.css({
982        '--vai-vh': Math.max(240, Math.round(height)) + 'px',
983        '--vai-vtop': Math.max(0, Math.round(top)) + 'px',
984        '--vai-vleft': Math.max(0, Math.round(left)) + 'px',
985        '--vai-vwidth': Math.max(280, Math.round(width)) + 'px'
986      });
987      $panel.toggleClass('is-keyboard-open', (window.innerHeight - height) > 120);
988    }
989
990    function settleMobileViewport() {
991      applyVisualViewport();
992      requestAnimationFrame(function(){
993        applyVisualViewport();
994        scrollBottom(true);
995      });
996      setTimeout(function(){ applyVisualViewport(); scrollBottom(true); }, 80);
997      setTimeout(function(){ applyVisualViewport(); scrollBottom(true); }, 280);
998    }
999
1000    function refreshNativeListingLayout() {
1001      var refresh = function() {
1002        try {
1003          // The desktop advisor changes the available page width with an
1004          // animated body padding. Slick keeps the slide widths calculated
1005          // before that change unless it is explicitly repositioned.
1006          $(window).triggerHandler('resize.voigtAiAdvisorLayout');
1007          $('.mls-project-listing-slider.slick-initialized:visible').each(function(){
1008            var $slider = $(this);
1009            try {
1010              $slider.slick('setPosition');
1011              var instance = $slider.slick('getSlick');
1012              if (instance && typeof instance.resize === 'function') instance.resize();
1013            } catch (e) {}
1014          });
1015          $(document).trigger('voigt:advisor:layout-updated');
1016        } catch (e) {}
1017      };
1018
1019      window.requestAnimationFrame(refresh);
1020      window.setTimeout(refresh, 60);
1021      window.setTimeout(refresh, 180);
1022      // body padding-left transitions for .28s on desktop.
1023      window.setTimeout(refresh, 330);
1024      window.setTimeout(refresh, 520);
1025    }
1026
1027    function openPanel(focus) {
1028      setUnreadCount(0);
1029      if (isMobile()) lockMobilePage();
1030      $root.addClass('is-open');
1031      $panel.addClass('is-open').attr('aria-hidden', 'false');
1032      $backdrop.addClass('is-open');
1033      document.body.classList.add('voigt-ai-browser-panel-open');
1034      document.body.classList.add('voigt-ai-open');
1035      hideCompetingMobileUi();
1036      document.documentElement.classList.toggle('voigt-ai-browser-lock', isMobile());
1037      document.body.classList.toggle('voigt-ai-browser-lock', isMobile());
1038      $('.voigt-ai-browser-teaser').prop('hidden', true);
1039      settleMobileViewport();
1040      refreshNativeListingLayout();
1041      if (focus !== false) {
1042        setTimeout(function(){
1043          $panel.find('textarea').trigger('focus');
1044          settleMobileViewport();
1045        }, 180);
1046      }
1047    }
1048
1049    function closePanel() {
1050      $panel.find('textarea,input').trigger('blur');
1051      $root.removeClass('is-open');
1052      $panel.removeClass('is-open is-handoff-open').attr('aria-hidden', 'true');
1053      $panel.find('.voigt-ai-browser-handoff').prop('hidden', true);
1054      $backdrop.removeClass('is-open');
1055      document.body.classList.remove('voigt-ai-browser-panel-open');
1056      document.body.classList.remove('voigt-ai-open');
1057      restoreCompetingMobileUi();
1058      document.documentElement.classList.remove('voigt-ai-browser-lock');
1059      document.body.classList.remove('voigt-ai-browser-lock');
1060      unlockMobilePage();
1061      applyVisualViewport();
1062      refreshNativeListingLayout();
1063    }
1064
1065    function authStatus(message, isError) {
1066      if (!$authModal.length) return;
1067      var text = String(message || '');
1068      var $status = $authModal.find('.voigt-ai-auth-status').first();
1069      $status
1070        .text(text)
1071        .prop('hidden', !text)
1072        .toggleClass('is-error', !!isError)
1073        .toggleClass('is-success', !isError && !!text);
1074      if (text) {
1075        var dialog = $authModal.find('.voigt-ai-auth-dialog').get(0);
1076        if (dialog && $status.get(0)) {
1077          var top = $status.get(0).offsetTop - 16;
1078          if (dialog.scrollTop > top || isError) dialog.scrollTo({top: Math.max(0, top), behavior: 'smooth'});
1079        }
1080      }
1081    }
1082
1083    function refreshAuthNonce(force) {
1084      var now = Date.now();
1085      if (!force && authNonce && authNonceFetchedAt && (now - authNonceFetchedAt) < 5 * 60 * 1000) {
1086        return $.Deferred().resolve(authNonce).promise();
1087      }
1088      if (authNonceRequest) return authNonceRequest;
1089      authNonceRequest = $.ajax({
1090        url: cfg.ajaxUrl,
1091        method: 'POST',
1092        dataType: 'json',
1093        cache: false,
1094        timeout: 12000,
1095        data: {action: 'voigt_ai_browser_auth_nonce', _: now}
1096      }).then(function(response){
1097        var fresh = response && response.success === true && response.data ? String(response.data.nonce || '') : '';
1098        if (fresh) {
1099          authNonce = fresh;
1100          authNonceFetchedAt = Date.now();
1101        }
1102        return authNonce;
1103      }, function(){
1104        return authNonce;
1105      }).always(function(){
1106        authNonceRequest = null;
1107      });
1108      return authNonceRequest;
1109    }
1110
1111    function setAuthTab(mode) {
1112      if (!$authModal.length) return;
1113      var allowed = ['login', 'lost', 'reset'];
1114      if (cfg.registrationEnabled) allowed.push('register');
1115      mode = allowed.indexOf(String(mode || 'login')) >= 0 ? String(mode || 'login') : 'login';
1116
1117      var $dialog = $authModal.find('.voigt-ai-auth-dialog').first();
1118      var special = mode === 'lost' || mode === 'reset';
1119      var title = special
1120        ? String($dialog.attr(mode === 'reset' ? 'data-title-reset' : 'data-title-lost') || '')
1121        : String($dialog.attr('data-title-default') || '');
1122      var subtitle = special
1123        ? String($dialog.attr(mode === 'reset' ? 'data-subtitle-reset' : 'data-subtitle-lost') || '')
1124        : String($dialog.attr('data-subtitle-default') || '');
1125      var previousMode = String($dialog.attr('data-auth-mode') || '');
1126      var modeChanged = previousMode !== mode;
1127      $dialog.attr('data-auth-mode', mode);
1128      if (modeChanged && $dialog.get(0)) $dialog.get(0).scrollTop = 0;
1129      $dialog.find('.voigt-ai-auth-title').text(title);
1130      $dialog.find('.voigt-ai-auth-subtitle').text(subtitle);
1131      $dialog.find('.voigt-ai-auth-benefits,.voigt-ai-auth-tabs').prop('hidden', special);
1132
1133      $authModal.find('.voigt-ai-auth-tab').each(function(){
1134        var active = String($(this).attr('data-auth-tab') || '') 
1134=== mode;
1135        $(this).toggleClass('is-active', active).attr('aria-selected', active ? 'true' : 'false');
1136      });
1137      $authModal.find('.voigt-ai-auth-form').each(function(){
1138        var active = String($(this).attr('data-auth-form') || '') === mode;
1139        $(this).toggleClass('is-active', active).prop('hidden', !active);
1140      });
1141      authStatus('', false);
1142      window.setTimeout(function(){
1143        var activeElement = document.activeElement;
1144        var activeInsideModal = !!(activeElement && $authModal.get(0) && $authModal.get(0).contains(activeElement));
1145        if (!modeChanged && activeInsideModal) return;
1146        var field = $authModal.find('.voigt-ai-auth-form.is-active input:not([type="checkbox"]):visible').first().get(0);
1147        if (!field) return;
1148        try { field.focus({preventScroll:true}); } catch (e) { field.focus(); }
1149      }, 80);
1150    }
1151
1152    function openAuthModal(mode) {
1153      if (!$authModal.length || cfg.loggedInUser || isAgentPanelContext()) return;
1154      mode = mode || 'login';
1155      if (!$authModal.prop('hidden')) {
1156        var currentMode = String($authModal.find('.voigt-ai-auth-dialog').first().attr('data-auth-mode') || 'login');
1157        if (currentMode !== mode) setAuthTab(mode);
1158        return;
1159      }
1160      if (mode === 'register' && turnstileRequired() && !turnstileSessionVerified && (!tsToken() || turnstileConsumed)) {
1161        ensureTurnstileReady(function(){ openAuthModal(mode); });
1162        return;
1163      }
1164      setAuthTab(mode);
1165      $authModal.prop('hidden', false).attr('aria-hidden', 'false').addClass('is-open');
1166      var dialog = $authModal.find('.voigt-ai-auth-dialog').get(0);
1167      if (dialog) dialog.scrollTop = 0;
1168      refreshAuthNonce(false);
1169      document.documentElement.classList.add('voigt-ai-auth-open');
1170      document.body.classList.add('voigt-ai-auth-open');
1171    }
1172
1173    function closeAuthModal() {
1174      if (!$authModal.length || authBusy) return;
1175      $authModal.removeClass('is-open').attr('aria-hidden', 'true').prop('hidden', true);
1176      document.documentElement.classList.remove('voigt-ai-auth-open');
1177      document.body.classList.remove('voigt-ai-auth-open');
1178      authStatus('', false);
1179    }
1180
1181    function authFormValue($form, name) {
1182      var $field = $form.find('[name="' + name + '"]').first();
1183      if (!$field.length) return '';
1184      if ($field.is(':checkbox')) return $field.is(':checked') ? '1' : '';
1185      return String($field.val() || '');
1186    }
1187
1188    function submitAuthForm($form, options) {
1189      options = options || {};
1190      if (authBusy || !$form.length) return;
1191      var mode = String($form.attr('data-auth-form') || 'login');
1192      var actionMap = {
1193        login: 'voigt_ai_browser_member_login',
1194        register: 'voigt_ai_browser_member_register',
1195        lost: 'voigt_ai_browser_member_lost_password',
1196        reset: 'voigt_ai_browser_member_reset_password'
1197      };
1198      var action = actionMap[mode] || actionMap.login;
1199      var email = authFormValue($form, 'email').trim();
1200      var password = authFormValue($form, 'password');
1201      var confirmation = authFormValue($form, 'password_confirm');
1202
1203      if ((mode === 'login' || mode === 'register' || mode === 'lost') && (!email || email.indexOf('@') < 1)) {
1204        authStatus(strings.auth_email_invalid || strings.error || '', true);
1205        return;
1206      }
1207      if ((mode === 'login' || mode === 'register' || mode === 'reset') && !password) {
1208        authStatus(strings.auth_invalid || strings.error || '', true);
1209        return;
1210      }
1211      if (mode === 'register' || mode === 'reset') {
1212        if (password.length < 8) {
1213          authStatus(strings.auth_password_short || strings.error || '', true);
1214          return;
1215        }
1216        if (password !== confirmation) {
1217          authStatus(strings.auth_password_mismatch || strings.error || '', true);
1218          return;
1219        }
1220      }
1221      if (mode === 'register' && !authFormValue($form, 'privacy')) {
1222        authStatus(strings.auth_privacy_required || strings.error || '', true);
1223        return;
1224      }
1225      if (mode === 'reset' && (!cfg.resetPassword || !cfg.resetPassword.key || !cfg.resetPassword.login)) {
1226        authStatus(strings.auth_reset_invalid || strings.error || '', true);
1227        return;
1228      }
1229
1230      // Registration also uses the Advisor's managed Turnstile session. A token
1231      // can expire while the account form is being completed, so reacquire it
1232      // before locking the form instead of failing silently on submit.
1233      if (mode === 'register' && turnstileRequired() && (!tsToken() || turnstileConsumed) && !options.turnstileRetried) {
1234        authStatus(strings.auth_working || nativeUiFallback('Please wait…', 'Bitte warten…', 'Proszę czekać…'), false);
1235        ensureTurnstileReady(function(){
1236          submitAuthForm($form, $.extend({}, options, {turnstileRetried: true}));
1237        });
1238        return;
1239      }
1240
1241      authBusy = true;
1242      authStatus(strings.auth_working || nativeUiFallback('Please wait…', 'Bitte warten…', 'Proszę czekać…'), false);
1243      $authModal.find('button,input').prop('disabled', true);
1244
1245      function unlockAuthForm() {
1246        authBusy = false;
1247        $authModal.find('button,input').prop('disabled', false);
1248      }
1249
1250      function requestAuth(nonceRetry) {
1251        return $.ajax({
1252          url: cfg.ajaxUrl,
1253          method: 'POST',
1254          dataType: 'json',
1255          cache: false,
1256          timeout: 30000,
1257          data: {
1258            action: action,
1259            nonce: authNonce || String(cfg.authNonce || ''),
1260            lang: currentLanguage(),
1261            redirect_url: window.location.href,
1262            session_id: sessionId,
1263            session_token: sessionToken,
1264            turnstile: tsToken(),
1265            name: authFormValue($form, 'name'),
1266            email: email,
1267            password: password,
1268            password_confirm: confirmation,
1269            remember: authFormValue($form, 'remember'),
1270            privacy: authFormValue($form, 'privacy'),
1271            website: authFormValue($form, 'website'),
1272            key: cfg.resetPassword && cfg.resetPassword.key ? cfg.resetPassword.key : '',
1273            login: cfg.resetPassword && cfg.resetPassword.login ? cfg.resetPassword.login : ''
1274          }
1275        }).done(function(response){
1276          if (!response || response.success !== true) {
1277            var payload = response && response.data ? response.data : {};
1278            var code = String(payload.code || '');
1279            var replacementNonce = String(payload.nonce || '');
1280            if (code === 'auth_session_expired' && !nonceRetry) {
1281              if (replacementNonce) {
1282                authNonce = replacementNonce;
1283                authNonceFetchedAt = Date.now();
1284                requestAuth(true);
1285              } else {
1286                refreshAuthNonce(true).always(function(){ requestAuth(true); });
1287              }
1288              return;
1289            }
1290            authStatus(payload.message || strings.error || nativeUiFallback('The operation could not be completed.', 'Der Vorgang konnte nicht abgeschlossen werden.', 'Nie udało się wykonać operacji.'), true);
1291            unlockAuthForm();
1292            return;
1293          }
1294          var data = response.data || {};
1295          if (mode === 'lost') {
1296            authStatus(data.message || strings.auth_reset_email_sent || '', false);
1297            $form.get(0).reset();
1298            unlockAuthForm();
1299            return;
1300          }
1301          var successMessage = mode === 'reset'
1302            ? strings.auth_reset_success
1303            : (mode === 'register' ? strings.auth_register_success : strings.auth_login_success);
1304          authStatus(data.message || successMessage || '', false);
1305          try {
1306            sessionStorage.setItem('voigtAiBrowserReopenAfterAuth', '1');
1307            sessionStorage.setItem('voigtAiBrowserAuthSuccess', mode);
1308          } catch (e) {}
1309          window.setTimeout(function(){
1310            window.location.assign(String(data.redirect || window.location.href));
1311          }, 550);
1312        }).fail(function(xhr){
1313          var payload = xhr && xhr.responseJSON && xhr.responseJSON.data ? xhr.responseJSON.data : {};
1314          var code = String(payload.code || '');
1315          var replacementNonce = String(payload.nonce || '');
1316          if (code === 'auth_session_expired' && !nonceRetry) {
1317            if (replacementNonce) {
1318              authNonce = replacementNonce;
1319              authNonceFetchedAt = Date.now();
1320              requestAuth(true);
1321            } else {
1322              refreshAuthNonce(true).always(function(){ requestAuth(true); });
1323            }
1324            return;
1325          }
1326          authStatus(payload.message || strings.error || nativeUiFallback('The operation could not be completed.', 'Der Vorgang konnte nicht abgeschlossen werden.', 'Nie udało się wykonać operacji.'), true);
1327          unlockAuthForm();
1328        });
1329      }
1330
1331      refreshAuthNonce(false).always(function(){ requestAuth(false); });
1332    }
1333
1334    function scrollBottom(immediate) {
1335      if (!$messages.length || !$messages.get(0)) return;
1336      var node = $messages.get(0);
1337      if (immediate) {
1338        $messages.stop(true);
1339        node.scrollTop = node.scrollHeight;
1340        return;
1341      }
1342      $messages.stop(true).animate({scrollTop: node.scrollHeight}, 160);
1343    }
1344
1345    function sameListingDestination(href) {
1346      if (!href || !getForm().length) return false;
1347      try {
1348        var target = new URL(href, window.location.href);
1349        var current = new URL(window.location.href);
1350        return target.origin === current.origin && target.pathname.replace(/\/+$/, '') === current.pathname.replace(/\/+$/, '');
1351      } catch (e) { return false; }
1352    }
1353
1354    function feedbackCategoryValues() {
1355      return ['misunderstood','wrong_filters','filters_not_applied','factual_error','other'];
1356    }
1357
1358    function submitFeedback(messageId, payload, $controls) {
1359      if (!messageId || !sessionId || !sessionToken) return;
1360      $controls.addClass('is-sending');
1361      $.ajax({
1362        url: cfg.ajaxUrl,
1363        type: 'POST',
1364        dataType: 'json',
1365        data: {
1366          action: 'voigt_ai_browser_feedback',
1367          nonce: cfg.nonce,
1368          session_id: sessionId,
1369          session_token: sessionToken,
1370          message_id: messageId,
1371          rating: payload.rating || 'down',
1372          category: payload.category || '',
1373          comment: payload.comment || '',
1374          correction: payload.correction || ''
1375        }
1376      }).done(function(response){
1377        if (!response || !response.success) return;
1378        $controls.removeClass('is-sending').addClass('is-saved');
1379        $controls.find('.voigt-ai-feedback-form').remove();
1380        $controls.find('button').prop('disabled', true);
1381        $('<span class="voigt-ai-feedback-thanks">').text(strings.feedback_thanks || nativeUiFallback('Thank you.', 'Danke.', 'Dziękujemy.')).appendTo($controls);
1382        conversationHistory.forEach(function(item){
1383          if (item.options && String(item.options.messageId || '') === String(messageId)) {
1384            item.options.feedback = payload;
1385          }
1386        });
1387        saveConversation();
1388      }).fail(function(){
1389        $controls.removeClass('is-sending');
1390      });
1391    }
1392
1393    function openFeedbackForm($controls, messageId) {
1394      if ($controls.find('.voigt-ai-feedback-form').length) return;
1395      var labels = Array.isArray(strings.feedback_categories) ? strings.feedback_categories : [];
1396      var values = feedbackCategoryValues();
1397      var $form = $('<div class="voigt-ai-feedback-form">');
1398      $('<strong>').text(strings.feedback_title || nativeUiFallback('What was wrong?', 'Was war falsch?', 'Co było nie tak?')).appendTo($form);
1399      var $select = $('<select>').attr('aria-label', strings.feedback_category || nativeUiFallback('Feedback category', 'Problemtyp', 'Rodzaj problemu')).appendTo($form);
1400      values.forEach(function(value, index){ $('<option>').val(value).text(labels[index] || value).appendTo($select); });
1401      $('<textarea rows="2" class="voigt-ai-feedback-comment">').attr('placeholder', strings.feedback_comment || '').appendTo($form);
1402      $('<textarea rows="2" class="voigt-ai-feedback-correction">').attr('placeholder', strings.feedback_correction || '').appendTo($form);
1403      var $actions = $('<div class="voigt-ai-feedback-form-actions">').appendTo($form);
1404      $('<button type="button" class="voigt-ai-feedback-submit">').text(strings.feedback_send || nativeUiFallback('Send', 'Senden', 'Wyślij')).on('click', function(){
1405        submitFeedback(messageId, {
1406          rating: 'down',
1407          category: String($select.val() || 'other'),
1408          comment: String($form.find('.voigt-ai-feedback-comment').val() || ''),
1409          correction: String($form.find('.voigt-ai-feedback-correction').val() || '')
1410        }, $controls);
1411      }).appendTo($actions);
1412      $('<button type="button" class="voigt-ai-feedback-cancel">').text(strings.feedback_cancel || nativeUiFallback('Cancel', 'Abbrechen', 'Anuluj')).on('click', function(){ $form.remove(); }).appendTo($actions);
1413      $controls.append($form);
1414      scrollBottom();
1415    }
1416
1417    function appendFeedbackControls($message, messageId, existing) {
1418      if (!messageId) return;
1419      existing = existing && typeof existing === 'object' ? existing : {};
1420      var $controls = $('<div class="voigt-ai-feedback">').attr('data-message-id', messageId);
1421      if (existing.rating) {
1422        $controls.addClass('is-saved');
1423        $('<span class="voigt-ai-feedback-thanks">').text(strings.feedback_thanks || nativeUiFallback('Thank you.', 'Danke.', 'Dziękujemy.')).appendTo($controls);
1424        $message.append($controls);
1425        return;
1426      }
1427      $('<button type="button" class="voigt-ai-feedback-up">').attr('aria-label', nativeUiFallback('Good answer', 'Gute Antwort', 'Dobra odpowiedź')).html('<span aria-hidden="true">👍</span><em></em>')
1428        .find('em').text(strings.feedback_good || nativeUiFallback('Good answer', 'Gute Antwort', 'Dobra odpowiedź')).end()
1429        .on('click', function(){ submitFeedback(messageId, {rating:'up'}, $controls); })
1430        .appendTo($controls);
1431      $('<button type="button" class="voigt-ai-feedback-down">').attr('aria-label', nativeUiFallback('Report wrong answer', 'Falsche Antwort melden', 'Zgłoś błędną odpowiedź')).html('<span aria-hidden="true">👎</span><em></em>')
1432        .find('em').text(strings.feedback_bad || nativeUiFallback('Report', 'Melden', 'Zgłoś')).end()
1433        .on('click', function(){ openFeedbackForm($controls, messageId); })
1434        .appendTo($controls);
1435      $message.append($controls);
1436    }
1437
1438    function addMessage(body, role, options) {
1439      options = options || {};
1440      var displayOptions = serialisableOptions(options);
1441      if (displayOptions.href && sameListingDestination(displayOptions.href)) displayOptions.href = '';
1442      var $message = $('<div class="voigt-ai-browser-message">').addClass(role === 'user' ? 'is-user' : 'is-ai');
1443      $('<div>').text(body || '').appendTo($message);
1444      if (displayOptions.small) $('<small>').text(displayOptions.small).appendTo($message);
1445      if (displayOptions.cta) {
1446        $('<button type="button" class="voigt-ai-browser-results-cta">')
1447          .text(displayOptions.cta)
1448          .on('click', function(){
1449            saveConversation();
1450            saveSession();
1451            if (displayOptions.href) {
1452              // Store cross-page criteria only after the user deliberately clicks
1453              // “View properties”. Previously they were stored as soon as the AI
1454              // answered, so a later manual visit to the listing page unexpectedly
1455              // launched a second search and looked like a double page reload.
1456              if (
1457                displayOptions.navigationCriteria
1458                && typeof displayOptions.navigationCriteria === 'object'
1459                && Object.keys(displayOptions.navigationCriteria).length
1460              ) {
1461                try {
1462                  var navigationId = 'vai-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
1463                  var navigationTarget = new URL(displayOptions.href, window.location.href);
1464                  sessionStorage.setItem('voigtAiPendingCriteria', JSON.stringify({
1465                    version: 4,
1466                    source: 'results_cta',
1467                    navigationId: navigationId,
1468                    targetPath: navigationTarget.pathname,
1469                    criteria: displayOptions.navigationCriteria,
1470                    silent: true,
1471                    scrollToResults: true,
1472                    createdAt: Date.now()
1473                  }));
1474                  displayOptions.navigationId = navigationId;
1475                } catch (e) {}
1476              }
1477              // Keep the session and transcript, close the panel, then navigate.
1478              // The destination applies the pending filters silently and remains closed.
1479              closePanel();
1480              try { sessionStorage.setItem('voigtAiPanelClosedForNavigation', '1'); } catch (e) {}
1481              window.setTimeout(function(){
1482                var destination = new URL(displayOptions.href, window.location.href);
1483                if (displayOptions.navigationId) {
1484                  destination.hash = 'voigt-ai-search=' + encodeURIComponent(displayOptions.navigationId);
1485                }
1486                if (typeof window.voigtReloadTraceLog === 'function') {
1487                  window.voigtReloadTraceLog('advisor-results-navigation', { targetUrl: destination.toString() });
1488                }
1489                window.location.assign(destination.toString());
1490              }, 20);
1491              return;
1492            }
1493            closePanel();
1494            scrollToResults();
1495          })
1496          .appendTo($message);
1497      }
1498      if (role !== 'user' && displayOptions.messageId) {
1499        appendFeedbackControls($message, displayOptions.messageId, displayOptions.feedback);
1500      }
1501      $messages.append($message);
1502      if (options.persist !== false) {
1503        conversationHistory.push({
1504          body: String(body || ''),
1505          role: role === 'user' ? 'user' : 'ai',
1506          options: displayOptions
1507        });
1508        conversationHistory = conversationHistory.slice(-40);
1509        saveConversation();
1510      }
1511      scrollBottom();
1512      if (
1513        role !== 'user' &&
1514        options.persist !== false &&
1515        !panelIsOpen() &&
1516        conversationHistory.some(function(item){ return item && item.role === 'user'; })
1517      ) {
1518        incrementUnreadCount();
1519      }
1520      return $message;
1521    }
1522
1523    function restoreConversation() {
1524      var payload = null;
1525      try {
1526        payload = JSON.parse(sessionStorage.getItem(conversationKey) || 'null');
1527        if (!payload) {
1528          var legacyPayload = JSON.parse(sessionStorage.getItem(legacyConversationKey) || 'null');
1529          var legacyPayloadLang = String(legacyPayload && legacyPayload.language || '').toLowerCase().slice(0, 2);
1530          if (legacyPayload && legacyPayloadLang === pageLanguage) {
1531            payload = legacyPayload;
1532            sessionStorage.setItem(conversationKey, JSON.stringify(legacyPayload));
1533          }
1534        }
1535      } catch (e) {}
1536      if (!payload || !Array.isArray(payload.messages) || !payload.messages.length) {
1537        $messages.empty();
1538        conversationHistory = [];
1539        addMessage(initialGreeting(), 'ai', {small: strings.hint || ''});
1540        return false;
1541      }
1542      // If the user explicitly reset the server session, do not revive an unrelated transcript.
1543      if (payload.session_id && sessionId && String(payload.session_id) !== String(sessionId)) {
1544        clearConversationStorage();
1545        $messages.empty();
1546        addMessage(initialGreeting(), 'ai', {small: strings.hint || ''});
1547        return false;
1548      }
1549      // A transcript is namespaced by page language. Never allow an old
1550      // transcript to change the language of the current TranslatePress page.
1551      conversationHistory = payload.messages.slice(-40).map(function(item){
1552        return {
1553          body: String(item && item.body || ''),
1554          role: item && item.role === 'user' ? 'user' : 'ai',
1555          options: serialisableOptions(item && item.options || {})
1556        };
1557      });
1558      currentSuggestions = Array.isArray(payload.suggestions) ? payload.suggestions.slice(0, 12) : [];
1559      $messages.empty();
1560      conversationHistory.forEach(function(item){
1561        addMessage(item.body, item.role, $.extend({}, item.options, {persist: false}));
1562      });
1563      return true;
1564    }
1565
1566
1567    function restoreConversationFromServer() {
1568      if (!sessionId || !sessionToken) return;
1569      $.ajax({
1570        url: cfg.ajaxUrl,
1571        type: 'POST',
1572        dataType: 'json',
1573        data: {
1574          action: 'voigt_ai_browser_restore',
1575          nonce: cfg.nonce,
1576          session_id: sessionId,
1577          session_token: sessionToken
1578        }
1579      }).done(function(response){
1580        if (!response || !response.success || !response.data) return;
1581        var data = response.data;
1582        if (data.turnstile_session_verified === true) {
1583          turnstileSessionVerified = true;
1584          clearTurnstileRefreshTimer();
1585          turnstileIssuedAt = 0;
1586          saveSession();
1587          syncTurnstileControls();
1588        }
1589        if (!Array.isArray(data.messages) || !data.messages.length) return;
1590        var serverLang = String(data.language || pageLanguage).toLowerCase().slice(0, 2);
1591        if (serverLang && serverLang !== pageLanguage) return;
1592        conversationHistory = data.messages.slice(-40).map(function(item){
1593          return {
1594            body: String(item && item.body || ''),
1595            role: item && item.role === 'user' ? 'user' : 'ai',
1596            options: serialisableOptions(item && item.options || {})
1597          };
1598        });
1599        // The server transcript starts with the first user turn. Restore the
1600        // introductory greeting locally when it is not present in the session.
1601        if (conversationHistory.length && conversationHistory[0].role === 'user') {
1602          conversationHistory.unshift({
1603            body: String(initialGreeting()),
1604            role: 'ai',
1605            options: serialisableOptions({small: strings.hint || ''})
1606          });
1607        }
1608        currentSuggestions = Array.isArray(data.suggestions) ? data.suggestions.slice(0, 12) : [];
1609        window.__voigtAiLastCriteria = data.criteria || window.__voigtAiLastCriteria || {};
1610        $messages.empty();
1611        conversationHistory.forEach(function(item){
1612          addMessage(item.body, item.role, $.extend({}, item.options, {persist: false}));
1613        });
1614        restoredConversation = true;
1615        $('.voigt-ai-browser-teaser').prop('hidden', true);
1616        renderSuggestions(currentSuggestions, {persist: false});
1617        saveSession();
1618        saveConversation();
1619        refreshSummary(window.__voigtAiLastCriteria);
1620      });
1621    }
1622
1623    function stopLoadingSequence() {
1624      if (loadingTimer) {
1625        window.clearTimeout(loadingTimer);
1626        loadingTimer = null;
1627      }
1628      loadingSequence = [];
1629      loadingSequenceIndex = 0;
1630    }
1631
1632    function startLoadingSequence(labels) {
1633      stopLoadingSequence();
1634      loadingSequence = (labels || []).filter(Boolean);
1635      if (!loadingNode || loadingSequence.length < 2) return;
1636      loadingSequenceIndex = 0;
1637      var advance = function(){
1638        if (!loadingNode || !loadingSequence.length) return;
1639        loadingSequenceIndex = Math.min(loadingSequenceIndex + 1, loadingSequence.length - 1);
1640        loadingNode.find('em').text(loadingSequence[loadingSequenceIndex] || '');
1641        if (loadingSequenceIndex < loadingSequence.length - 1) loadingTimer = window.setTimeout(advance, 1850);
1642      };
1643      loadingTimer = window.setTimeout(advance, 1650);
1644    }
1645
1646    function loadingLabelsForMessage(message, quickAction) {
1647      var raw = String(message || '');
1648      var text = raw.toLowerCase();
1649      try { text = text.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } catch (e) {}
1650      var labels = [];
1651      var has = function(pattern){ return pattern.test(text); };
1652      if (quickAction === 'contact' || has(/advisor|doradc|asesor|berater|human|contact/)) {
1653        labels = [strings.thinking || '', strings.thinking_prepare || ''];
1654      } else if (has(/compare|comparison|porown|vergleich|comparar|ref[ ._-]*id|\br[0-9]{3,}\b/)) {
1655        labels = [strings.thinking_compare || strings.thinking || '', strings.thinking_property || '', strings.thinking_prepare || ''];
1656      } else if (has(/alert|notify|notification|powiadom|zapisz wyszukiwanie|favorite|favourite|ulubion|price drop|obniz|bajada de precio|preissenkung/)) {
1657        labels = [strings.thinking_alert || strings.thinking || '', strings.thinking_prepare || ''];
1658      } else if (has(/mortgage|financ|kredyt|hipotec|hypothek|tax|podatek|impuesto|steuer|cost|koszt|gastos|kosten/)) {
1659        labels = [strings.thinking_financing || strings.thinking || '', strings.thinking_process || '', strings.thinking_prepare || ''];
1660      } else if (has(/location|area|district|neighbourhood|neighborhood|lokaliz|okolica|dzielnic|zona|gebiet|marbella|estepona|mijas|malaga|benahavis|sotogrande/)) {
1661        labels = [strings.thinking_location || strings.thinking || '', strings.thinking_prepare || ''];
1662      } else if (has(/budget|price|cena|eur|euro|\b[0-9]+\s*[km]\b|presupuesto|precio|budget|preis/)) {
1663        labels = [strings.thinking_budget || strings.thinking || '', strings.thinking_property || '', strings.thinking_prepare || ''];
1664      } else if (has(/buy|purchase|property|villa|apartment|house|kup|nieruchom|willa|apartament|comprar|propiedad|immobilie|kaufen/)) {
1665        labels = [strings.thinking_property || strings.thinking || '', strings.thinking_prepare || ''];
1666      } else {
1667        labels = [strings.thinking || '', strings.thinking_prepare || ''];
1668      }
1669      return labels.filter(Boolean);
1670    }
1671
1672    function addLoading(label) {
1673      removeLoading();
1674      loadingNode = $('<div class="voigt-ai-browser-message is-ai is-loading" role="status" aria-live="polite" aria-atomic="true"><span></span><span></span><span></span><em></em></div>');
1675      var labels = Array.isArray(label) ? label.filter(Boolean) : [label || strings.thinking || ''];
1676      loadingNode.find('em').text(labels[0] || '');
1677      $messages.append(loadingNode);
1678      startLoadingSequence(labels);
1679      scrollBottom();
1680    }
1681
1682    function updateLoading(label) {
1683      if (!loadingNode) return;
1684      var labels = Array.isArray(label) ? label.filter(Boolean) : [label || ''];
1685      loadingNode.find('em').text(labels[0] || '');
1686      startLoadingSequence(labels);
1687    }
1688
1689    function removeLoading() {
1690      stopLoadingSequence();
1691      if (loadingNode) {
1692        loadingNode.remove();
1693        loadingNode = null;
1694      }
1695    }
1696
1697    function selectedValues($element) {
1698      if (!$element || !$element.length) return [];
1699      if ($element.first().is(':checkbox,:radio')) {
1700        return $element.filter(':checked').map(function(){ return this.value; }
1700).get().filter(Boolean);
1701      }
1702      var value = $element.val();
1703      return Array.isArray(value) ? value : (value ? [value] : []);
1704    }
1705
1706    function nativeChange($element) {
1707      if (!$element || !$element.length) return;
1708      $element.each(function(){
1709        try {
1710          this.dispatchEvent(new Event('change', {bubbles: true}));
1711        } catch (e) {
1712          $(this).trigger('change');
1713        }
1714      });
1715    }
1716
1717    function syncSelectWidget($select) {
1718      if (!$select || !$select.length) return;
1719      $select.each(function(){
1720        var select = this;
1721        var selected = new Set(Array.prototype.slice.call(select.selectedOptions || []).map(function(o){ return String(o.value); }));
1722        var $select = $(select);
1723        var $simpleWrapper = $select.closest('.mls-multi-wrapper');
1724        if ($simpleWrapper.length) {
1725          $simpleWrapper.find('.mls-multi-list input[type="checkbox"]').each(function(){ this.checked = selected.has(String(this.value)); });
1726          $simpleWrapper.find('.mls-multi-list li[data-value]').each(function(){
1727            $(this).toggleClass('mls-selected', selected.has(String($(this).attr('data-value'))));
1728          });
1729        }
1730        var $typeWrapper = $select.closest('.mls-select-wrapper');
1731        if ($typeWrapper.length) {
1732          $typeWrapper.find('.mls-options input[type="checkbox"]').each(function(){ this.checked = selected.has(String(this.value)); });
1733        }
1734      });
1735    }
1736
1737    function setSelectValues($select, values) {
1738      values = (values || []).map(String);
1739      if (!$select || !$select.length) return;
1740      $select.find('option').each(function(){
1741        this.selected = values.indexOf(String(this.value)) !== -1;
1742      });
1743      nativeChange($select);
1744      syncSelectWidget($select);
1745    }
1746
1747    function rateData() {
1748      var $wrapper = getForm().find('.slider-range-wrapper').first();
1749      return {
1750        rate: parseFloat($wrapper.data('rate')) || 1,
1751        currency: String($wrapper.data('currency') || 'EUR'),
1752        min: num($wrapper.data('min')),
1753        max: num($wrapper.data('max'))
1754      };
1755    }
1756
1757    function currentCriteria() {
1758      if (!getForm(true).length) {
1759        return window.__voigtAiLastCriteria || pendingCriteria || {};
1760      }
1761
1762      var locations = selectedValues($form.find('#mls_search_parent_area'));
1763      var $hidden = $form.find('#mls_search_area_hidden');
1764      if (!locations.length && $hidden.length && $hidden.val()) {
1765        locations = String($hidden.val()).split(',').map(function(item){ return $.trim(item); }).filter(Boolean);
1766      }
1767      if (!locations.length) locations = selectedValues($form.find('#mls_search_area'));
1768
1769      var currency = rateData();
1770      var minPrice = num($form.find('.min_price').val());
1771      var maxPrice = num($form.find('.max_price').val());
1772      if (currency.currency !== 'EUR' && currency.rate !== 1) {
1773        if (minPrice !== null) minPrice = Math.round(minPrice / currency.rate);
1774        if (maxPrice !== null) maxPrice = Math.round(maxPrice / currency.rate);
1775      }
1776      if (currency.min !== null && minPrice === currency.min) minPrice = null;
1777      if (currency.max !== null && maxPrice === currency.max) maxPrice = null;
1778
1779      var filterType = $form.find('input[name="filter_type"]:checked').val() || $form.find('input[name="filter_type"]').val() || 'sales';
1780      var newMode = String($form.find('input[name="newdevelopment"]').val() || 'include');
1781      if (filterType === 'new_development') newMode = 'only';
1782      var features = selectedValues(getFeatureInputs().filter(':checked'));
1783      features = uniq(features.concat(selectedValues($form.find('input.voigt-ai-feature-mirror'))));
1784      var $featureMode = getFilterScope().find('#mls_search_features_search_type').first();
1785      var featureMode = String(($featureMode.length ? $featureMode.val() : $form.find('#mls_search_features_search_type').val()) || '2');
1786
1787      return {
1788        locations: uniq(locations),
1789        excluded_locations: [],
1790        area_scope: locations.length ? 'selected' : 'all_costa_del_sol',
1791        province: null,
1792        property_types: uniq(selectedValues($form.find('#mls_search_type'))),
1793        must_have_features: featureMode === '2' ? uniq(features) : [],
1794        preferred_features: featureMode === '2' ? [] : uniq(features),
1795        soft_criteria: [],
1796        min_bedrooms: num($form.find('#mls_search_beds').val()),
1797        exact_bedrooms: null,
1798        min_bathrooms: num($form.find('#mls_search_baths').val()),
1799        exact_bathrooms: null,
1800        min_price_eur: minPrice,
1801        max_price_eur: maxPrice,
1802        min_built_m2: null,
1803        max_built_m2: null,
1804        min_plot_m2: null,
1805        max_plot_m2: null,
1806        energy_rating: 'any',
1807        include_half_bathrooms: false,
1808        include_rented: false,
1809        rental_license: 'any',
1810        rental_date_from: null,
1811        rental_date_to: null,
1812        decree218: 'any',
1813        reference_ids: [],
1814        transaction_type: filterType === 'long_rentals' ? 'long_rental' : (filterType === 'short_rentals' ? 'short_rental' : 'sale'),
1815        new_development_mode: newMode,
1816        new_development_only: newMode === 'only',
1817        summary: ''
1818      };
1819    }
1820
1821    function fold(value) {
1822      var text = String(value || '').trim().toLowerCase();
1823      try { return text.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); } catch (e) { return text; }
1824    }
1825
1826    function optionMatch($select, values) {
1827      values = values || [];
1828      var queries = values.map(fold).filter(Boolean);
1829      var matched = [];
1830      $select.find('option').each(function(){
1831        var optionValue = String(this.value);
1832        var valueFolded = fold(optionValue);
1833        var labelFolded = fold($(this).text());
1834        queries.forEach(function(query){
1835          if (valueFolded === query || labelFolded === query) matched.push(optionValue);
1836        });
1837      });
1838      return uniq(matched);
1839    }
1840
1841    function applyLocations(criteria) {
1842      var locations = criteria.area_scope === 'all_costa_del_sol' ? [] : (criteria.locations || []);
1843      var $container = $form.find('.mls-location-group-container');
1844      if (!$container.length) {
1845        var $area = $form.find('#mls_search_area');
1846        setSelectValues($area, optionMatch($area, locations));
1847        return;
1848      }
1849
1850      var groups = $container.data('groups') || [];
1851      if (typeof groups === 'string') {
1852        try { groups = JSON.parse(groups); } catch (e) { groups = []; }
1853      }
1854      var parents = [];
1855      var children = [];
1856      var wholeParents = [];
1857      locations.forEach(function(location){
1858        var found = false;
1859        groups.forEach(function(group){
1860          if (fold(group.parent) === fold(location)) {
1861            parents.push(group.parent);
1862            wholeParents.push(group.parent);
1863            children = children.concat(group.children || []);
1864            found = true;
1865            return;
1866          }
1867          (group.children || []).forEach(function(child){
1868            if (fold(child) === fold(location)) {
1869              parents.push(group.parent);
1870              children.push(child);
1871              found = true;
1872            }
1873          });
1874        });
1875        if (!found) children.push(location);
1876      });
1877      parents = uniq(parents);
1878      children = uniq(children);
1879      wholeParents = uniq(wholeParents);
1880
1881      var $parent = $form.find('#mls_search_parent_area');
1882      var $child = $form.find('#mls_search_child_area');
1883
1884      // Use a real DOM change event. The custom multiselect listens through
1885      // addEventListener, so jQuery.trigger('change') alone does not reliably
1886      // refresh the visible parent tags.
1887      var matchedParents = optionMatch($parent, parents);
1888      setSelectValues($parent, matchedParents);
1889
1890      // main.js rebuilds child options from the selected parent(s). Re-query the
1891      // child select after that operation, then select the provider locations
1892      // used by the API while keeping the visible parent tag selected.
1893      $child = $form.find('#mls_search_child_area');
1894      if ($child.length) {
1895        var desiredChildren = children.slice();
1896        if (!locations.length) desiredChildren = [];
1897        setSelectValues($child, optionMatch($child, desiredChildren));
1898      }
1899
1900      $container.attr('data-selected-parents', JSON.stringify(matchedParents)).data('selected-parents', matchedParents);
1901      $container.attr('data-selected-children', JSON.stringify(children)).data('selected-children', children);
1902      $form.find('#mls_search_area_hidden').val(children.join(',')).trigger('change');
1903      nativeChange($parent);
1904    }
1905
1906    function setPrice(eurMin, eurMax) {
1907      var currency = rateData();
1908      var minValue = eurMin !== null && eurMin !== undefined ? Math.round(eurMin * currency.rate) : '';
1909      var maxValue = eurMax !== null && eurMax !== undefined ? Math.round(eurMax * currency.rate) : '';
1910      var $group = $form.find('.slider-range-wrapper').first().closest('.mls-form-group');
1911      var $min = $group.find('.min_price').first();
1912      var $max = $group.find('.max_price').first();
1913
1914      $min.val(minValue).trigger('input').trigger('change').trigger('blur');
1915      $max.val(maxValue).trigger('input').trigger('change').trigger('blur');
1916
1917      var $slider = $group.find('.slider-range').first();
1918      if ($slider.length && $.fn.slider && $slider.hasClass('ui-slider')) {
1919        var lower = minValue !== '' ? minValue : parseInt($slider.slider('option', 'min'), 10);
1920        var upper = maxValue !== '' ? maxValue : parseInt($slider.slider('option', 'max'), 10);
1921        try { $slider.slider('values', [lower, upper]); } catch (e) {}
1922      }
1923
1924      // Reuse the native Done handler. It performs the same validation and
1925      // currency/locale formatting as a real user leaving the price fields.
1926      var $done = $group.find('.price-range-done').first();
1927      if ($done.length) $done.trigger('click');
1928      else {
1929        var display = [];
1930        if (minValue !== '') display.push(Number(minValue).toLocaleStr
1930ing());
1931        if (maxValue !== '') display.push(Number(maxValue).toLocaleString());
1932        $group.find('.pricerangeResults,.pricerangeDisplay').val(display.join(' - ')).trigger('change');
1933      }
1934    }
1935
1936    function applyCriteria(criteria, options) {
1937      criteria = criteria || {};
1938      options = options || {};
1939      window.__voigtAiLastCriteria = criteria;
1940      pendingCriteria = criteria;
1941
1942      if (!getForm().length) {
1943        // On pages without the native property-search form we can still keep the
1944        // Advisor's criteria in memory for the next conversational turn. An
1945        // update-only turn (for example: the Advisor asks for a missing location
1946        // after an already explicit "find me..." request) must NOT pretend that
1947        // a search has started or add a misleading "View properties" CTA.
1948        // The CTA is reserved for an actual search/navigation request.
1949        refreshSummary(criteria);
1950        removeLoading();
1951        if (options.submit === false) return;
1952        addMessage(strings.applied_generic || '', 'ai', {
1953          cta: (strings.view_results || nativeUiFallback('View properties', 'Immobilien ansehen', 'Zobacz nieruchomości')).replace('%d', '').trim(),
1954          href: cfg.listingUrl,
1955          navigationCriteria: criteria
1956        });
1957        return;
1958      }
1959
1960      var filterType = criteria.transaction_type === 'long_rental' ? 'long_rentals' : (criteria.transaction_type === 'short_rental' ? 'short_rentals' : 'sales');
1961      var $radio = $form.find('input[name="filter_type"][value="' + filterType + '"]');
1962      if ($radio.length) $radio.prop('checked', true).trigger('change');
1963      else $form.find('input[name="filter_type"]').val(filterType);
1964
1965      $form.find('input[name="newdevelopment"]').val(criteria.new_development_mode || 'include');
1966      applyLocations(criteria);
1967      var $type = $form.find('#mls_search_type');
1968      setSelectValues($type, optionMatch($type, criteria.property_types || []));
1969      var beds = criteria.exact_bedrooms || criteria.min_bedrooms || '';
1970      var baths = criteria.exact_bathrooms || criteria.min_bathrooms || '';
1971      setSelectValues($form.find('#mls_search_beds'), beds ? [String(beds)] : []);
1972      setSelectValues($form.find('#mls_search_baths'), baths ? [String(baths)] : []);
1973      setPrice(criteria.min_price_eur, criteria.max_price_eur);
1974
1975      // More Filters may be injected after the Agent Panel tab becomes visible.
1976      // The final native submit is therefore delayed until the real checkbox
1977      // controls exist and have followed their normal click/change path.
1978
1979      refreshSummary(criteria);
1980      if (options.submit === false) {
1981        applyMoreFiltersWhenReady(criteria, function(appliedFeatures){
1982          ensureFeatureSubmissionMirrors(appliedFeatures);
1983          pendingCriteria = criteria;
1984          window.__voigtAiLastCriteria = criteria;
1985          refreshSummary(criteria);
1986          removeLoading();
1987        });
1988        return;
1989      }
1990      pendingSearch = true;
1991      pendingSearchContext = {
1992        silent: options.silent === true,
1993        scrollToResults: options.scrollToResults === true
1994      };
1995      pendingSearchId = 'vai-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
1996      $form.find('input[name="_voigt_ai_request_id"]').remove();
1997      $('<input type="hidden" name="_voigt_ai_request_id">').val(pendingSearchId).appendTo($form);
1998      if (pendingSearchTimer) window.clearTimeout(pendingSearchTimer);
1999      pendingSearchTimer = window.setTimeout(function(){
2000        if (!pendingSearch) return;
2001        var context = pendingSearchContext || {};
2002        pendingSearch = false;
2003        pendingSearchId = '';
2004        pendingSearchContext = {silent: false, scrollToResults: false};
2005        $form.find('input[name="_voigt_ai_request_id"]').remove();
2006        removeLoading();
2007        if (!context.silent) addMessage(strings.results_ready || str
2007ings.applied_generic || '', 'ai');
2008        refreshSummary();
2009        if (context.scrollToResults) window.setTimeout(scrollToResults, 100);
2010      }, 30000);
2011      if (!options.silent) updateLoading(strings.searching || '');
2012      applyMoreFiltersWhenReady(criteria, function(appliedFeatures){
2013        ensureFeatureSubmissionMirrors(appliedFeatures);
2014        var $submitForm = getForm();
2015        if ($submitForm.length) $submitForm.trigger('submit');
2016      });
2017    }
2018
2019    function labelForSelect($select, value) {
2020      var $option = $select.find('option[value="' + String(value).replace(/"/g, '\\"') + '"]');
2021      return $option.length ? $.trim($option.text()) : String(value);
2022    }
2023
2024    function criteriaLabels(criteria) {
2025      criteria = criteria || {};
2026      var output = [];
2027      if (criteria.transaction_type === 'long_rental') output.push('Long-term rent');
2028      else if (criteria.transaction_type === 'short_rental') output.push('Holiday rent');
2029      if (criteria.new_development_mode === 'only') output.push('New development');
2030      (criteria.locations || []).slice(0, 2).forEach(function(item){ output.push(item); });
2031      (criteria.property_types || []).slice(0, 2).forEach(function(item){
2032        output.push(getForm().length ? labelForSelect($form.find('#mls_search_type'), item) : item);
2033      });
2034      if (criteria.min_bedrooms) output.push(criteria.min_bedrooms + '+ beds');
2035      if (criteria.min_bathrooms) output.push(criteria.min_bathrooms + '+ baths');
2036      if (criteria.min_price_eur) output.push('≥ €' + Number(criteria.min_price_eur).toLocaleString());
2037      if (criteria.max_price_eur) output.push('≤ €' + Number(criteria.max_price_eur).toLocaleString());
2038      [].concat(criteria.must_have_features || [], criteria.preferred_features || []).slice(0, 3).forEach(function(param){
2039        if (!getForm().length) {
2040          output.push(param);
2041          return;
2042        }
2043        var label = featureLabel(param);
2044        if (label) output.push(label);
2045      });
2046      return uniq(output).slice(0, 10);
2047    }
2048
2049    function refreshSummary(criteria) {
2050      criteria = criteria || currentCriteria();
2051      var labels = criteriaLabels(criteria);
2052      var $wrappers = $('.voigt-ai-browser-applied');
2053      $wrappers.each(function(){
2054        var $wrapper = $(this);
2055        var $chips = $wrapper.find('.voigt-ai-browser-chips').empty();
2056        labels.forEach(function(label){ $('<span>').text(label).appendTo($chips); });
2057        $wrapper.prop('hidden', !labels.length);
2058      });
2059    }
2060
2061    function renderSuggestions(items, options) {
2062      options = options || {};
2063      currentSuggestions = (items || []).map(function(item){
2064        return {
2065          label: String(item && (item.label || item.message) || ''),
2066          message: String(item && (item.message || item.label) || ''),
2067          action: String(item && item.action || '')
2068        };
2069      }).filter(function(item){ return item.label || item.message; }).slice(0, 12);
2070      var $box = $panel.find('.voigt-ai-browser-suggestions');
2071      var $inner = $box.find('div').empty();
2072      currentSuggestions.forEach(function(item){
2073        $('<button type="button">')
2074          .text(item.label || item.message)
2075          .on('click', function(){
2076            if (item.action === 'agency_contact_form') { focusAgencyContactForm(); return; }
2077            sendMessage(item.message || item.label, item.action || '');
2078          })
2079          .appendTo($inner);
2080      });
2081      $box.toggle(!!$inner.children().length);
2082      if (options.persist !== false) saveConversation();
2083    }
2084
2085    function focusAgencyContactForm() {
2086      // Use the existing property enquiry form, with the chat's agency form
2087      // as a fallback on pages that do not contain a property contact section.
2088      var target = document.getElementById('contact-from');
2089      if (target && target.getClientRects().length) {
2090        closePanel();
2091        window.setTimeout(function(){
2092          target.style.scrollMarginTop = '110px';
2093          target.scrollIntoView({behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', block: 'start'});
2094          var field = target.querySelector('input:not([type="hidden"]):not([disabled]), textarea:not([disabled])');
2095          if (field) { field.focus({preventScroll: true}); }
2096        }, 100);
2097      } else {
2098        openHandoff();
2099        var field = $panel.find('.voigt-ai-browser-handoff input:visible:not([type="hidden"])').get(0);
2100        if (field) { field.focus(); }
2101      }
2102    }
2103
2104    function openHandoff() {
2105      var $handoff = $panel.find('.voigt-ai-browser-handoff');
2106      $handoff.prop('hidden', false);
2107      $panel.addClass('is-handoff-open');
2108      settleMobileViewport();
2109    }
2110
2111    function closeHandoff() {
2112      $panel.find('.voigt-ai-browser-handoff').prop('hidden', true);
2113      $panel.removeClass('is-handoff-open');
2114      $panel.find('.voigt-ai-browser-lead-status').text('');
2115      settleMobileViewport();
2116    }
2117
2118    function displayedPropertyRefs() {
2119      var refs = [];
2120      var seen = new Set();
2121      var addRef = function(value){
2122        var text = String(value || '').toUpperCase();
2123        var matches = text.match(/\bR\d{3,}\b/g) || [];
2124        matches.forEach(function(ref){
2125          if (!seen.has(ref)) { seen.add(ref); refs.push(ref); }
2126        });
2127      };
2128
2129      // The visible listing cards are authoritative for questions such as
2130      // "which of these is closest to the beach?". Read only cards currently
2131      // rendered in the list; do not infer refs from old chat results.
2132      $('.mls-pro-list-wrapper .mls-property-box:visible').each(function(){
2133        var $card = $(this);
2134        addRef($card.attr('data-ref'));
2135        addRef($card.attr('data-property-ref'));
2136        $card.find('.mls-pyc-right span,[data-ref],[data-property-ref]').each(function(){
2137          addRef($(this).attr('data-ref'));
2138          addRef($(this).attr('data-property-ref'));
2139          addRef($(this).text());
2140        });
2141      });
2142
2143      // Some list templates expose the reference outside .mls-pyc-right.
2144      if (!refs.length) {
2145        $('.mls-pro-list-wrapper:visible').find('[data-ref],[data-property-ref],.mls-pyc-right span').each(function(){
2146          addRef($(this).attr('data-ref'));
2147          addRef($(this).attr('data-property-ref'));
2148          addRef($(this).text());
2149        });
2150      }
2151      return refs.slice(0, 40);
2152    }
2153
2154    function availableFeatureDictionary() {
2155      var rows = [];
2156      var seen = new Set();
2157
2158      // Prefer the exact controls rendered by the current native MLS form.
2159      getFeatureInputs(true).each(function(){
2160        var $input = $(this);
2161        var id = String(this.value || '').trim();
2162        var name = $.trim($input.next('label').text());
2163        var $section = $input.closest('.mls-af-accodian-cnts');
2164        var category = $.trim($section.prev('.mls-af-accodian-title').text());
2165        if (!id || !name || seen.has(id)) return;
2166        seen.add(id);
2167        rows.push({id: id, name: name, category: category});
2168      });
2169
2170      // Agent Panel/page-builder tabs can initialise the advisor before the
2171      // hidden More Filters markup is visible. The server-localised registry
2172      // contains the same native checkbox IDs, so the AI still receives a
2173      // complete closed catalogue instead of an empty list. DOM rows above
2174      // always take precedence.
2175      Object.keys(cfg.featureLookup || {}
2175).forEach(function(id){
2176        id = String(id || '').trim();
2177        var meta = cfg.featureLookup[id] || {};
2178        var name = String(meta.name || '').trim();
2179        if (!id || !name || seen.has(id)) return;
2180        seen.add(id);
2181        rows.push({id: id, name: name, category: String(meta.category || '').trim()});
2182      });
2183
2184      return rows.slice(0, 300);
2185    }
2186
2187    function sendMessage(message, quickAction, turnstileConfirmed, retryExistingMessage) {
2188      message = $.trim(message || '');
2189      if (!message) return;
2190      if (quotaBlocked) {
2191        updateQuotaNotice(currentQuota);
2192        return;
2193      }
2194      if (!turnstileConfirmed && !ensureTurnstileReady(function(){ sendMessage(message, quickAction, true, retryExistingMessage); })) return;
2195      openPanel(false);
2196      if (!retryExistingMessage) {
2197        addMessage(message, 'user');
2198        $panel.find('textarea').val('').css('height', '48px');
2199      }
2200      addLoading(loadingLabelsForMessage(message, quickAction));
2201      var requestTurnstileToken = tsToken();
2202      if (turnstileRequired()) {
2203        $panel.find('input[name="cf-turnstile-response"]').val('');
2204        turnstileConsumed = true;
2205        syncTurnstileControls();
2206      }
2207
2208      $.ajax({
2209        url: cfg.ajaxUrl,
2210        type: 'POST',
2211        dataType: 'json',
2212        data: {
2213          action: 'voigt_ai_browser_chat',
2214          nonce: cfg.nonce,
2215          message: message,
2216          quick_action: quickAction || '',
2217          lang: currentLanguage(),
2218          current_criteria: JSON.stringify(currentCriteria()),
2219          available_features: JSON.stringify(availableFeatureDictionary()),
2220          displayed_refs: JSON.stringify(displayedPropertyRefs()),
2221          property_context: propertyContextPayload(),
2222          session_id: sessionId,
2223          session_token: sessionToken,
2224          agent_token: authenticatedAgentToken(),
2225          agent_context: isAgentPanelContext() ? '1' : '0',
2226          origin_url: location.href,
2227          turnstile: requestTurnstileToken,
2228          website: $panel.find('[name="website"]').val() || ''
2229        }
2230      }).done(function(response){
2231        removeLoading();
2232        if (!response || !response.success) {
2233          tsReset();
2234          var failedData = response && response.data ? response.data : {};
2235          if (failedData.quota) {
2236            failedData.quota.login_url = failedData.login_url || cfg.loginUrl || '';
2237            failedData.quota.register_url = failedData.register_url || cfg.registerUrl || '';
2238            updateQuotaNotice(failedData.quota);
2239          }
2240          addMessage(failedData.message || strings.error, 'ai');
2241          return;
2242        }
2243        var data = response.data || {};
2244        if (data.quota) {
2245          data.quota.login_url = data.login_url || cfg.loginUrl || '';
2246          data.quota.register_url = data.register_url || cfg.registerUrl || '';
2247          updateQuotaNotice(data.quota);
2248        }
2249        sessionId = data.session_id || sessionId;
2250        sessionToken = data.session_token || sessionToken;
2251        if (data.turnstile_session_verified === true) {
2252          turnstileSessionVerified = true;
2253          turnstilePendingAction = null;
2254          turnstileConsumed = false;
2255          clearTurnstileRefreshTimer();
2256          turnstileIssuedAt = 0;
2257          syncTurnstileControls();
2258        } else {
2259          tsReset();
2260        }
2261        saveSession();
2262        var aiFailureReason = data.ai_failure_reason || data.ai_diagnostic || '';
2263        if (aiFailureReason && window.console && console.error) { console.error('[voigt-ai] Agent diagnostic:', aiFailureReason); }
2264        addMessage(data.assistant_message || '', 'ai', {messageId: data.assistant_message_id || ''});
2265        renderSuggestions(data.suggestions || []);
2266        // The backend returns open_handoff only after an explicit request for a human advisor.
2267        if (data.open_handoff === true) openHandoff();
2268        if (data.timings && isAgentPanelContext() && window.console && console.info) {
2269          console.info('[voigt-ai] Timings:', data.timings, 'pending:', data.pending_question || 'none');
2270        }
2271        if (data.should_apply || data.apply_mode === 'search') {
2272          addLoading(strings.searching || '');
2273          applyCriteria(data.criteria || {});
2274        } else if (data.should_update_form || data.apply_mode === 'update') {
2275          applyCriteria(data.criteria || {}, {submit: false, silent: true});
2276        } else {
2277          window.__voigtAiLastCriteria = data.criteria || currentCriteria();
2278          refreshSummary(window.__voigtAiLastCriteria);
2279        }
2280      }).fail(function(xhr){
2281        removeLoading();
2282        var data = xhr.responseJSON && xhr.responseJSON.data ? xhr.responseJSON.data : {};
2283        if (data.code === 'turnstile_required') {
2284          turnstileSessionVerified = false;
2285          stored.turnstileVerified = false;
2286          saveSession();
2287          turnstileConsumed = true;
2288          syncTurnstileControls();
2289          scheduleTurnstileReset(50, true);
2290          turnstilePendingAction = function(){ sendMessage(message, quickAction, true, true); };
2291          return;
2292        }
2293        tsReset();
2294        if (data.code === 'agent_session_invalid') {
2295          try { localStorage.removeItem('vcsAgentToken'); } catch (e) {}
2296          try { document.documentElement.classList.remove('vcs-agent-panel-authenticated'); } catch (e) {}
2297        }
2298        if (data.diagnostic && window.console && console.error) {
2299          console.error('[voigt-ai] Agent diagnostic:', data.diagnostic);
2300        }
2301        if (data.quota) {
2302          data.quota.login_url = data.login_url || cfg.loginUrl || '';
2303          data.quota.register_url = data.register_url || cfg.registerUrl || '';
2304          updateQuotaNotice(data.quota);
2305        }
2306        addMessage(data.message || strings.error, 'ai');
2307      });
2308    }
2309
2310    function localFeatureLabel(param) {
2311      return featureLabel(param);
2312    }
2313
2314    function formatEuro(value) {
2315      value = parseInt(value, 10) || 0;
2316      try { return new Intl.NumberFormat(currentLanguage(), {style:'currency', currency:'EUR', maximumFractionDigits:0}).format(value); }
2317      catch (e) { return '€' + Number(value).toLocaleString(); }
2318    }
2319
2320    function relaxationButtonLabel(item) {
2321      var count = parseInt(item.count, 10) || 0;
2322      var base = '';
2323      if (item.type === 'feature') base = String(strings.relax_feature || nativeUiFallback('Remove requirement: %s', 'Anforderung entfernen: %s', 'Usuń wymaganie: %s')).replace('%s', localFeatureLabel(item.feature));
2324      else if (item.type === 'type') base = strings.relax_type || nativeUiFallback('Broaden the property type', 'Immobilientyp erweitern', 'Poszerz typ nieruchomości');
2325      else if (item.type === 'beds') base = String(strings.relax_beds || nativeUiFallback('Reduce minimum bedrooms to %d', 'Mindestanzahl der Schlafzimmer auf %d reduzieren', 'Zmniejsz minimalną liczbę sypialni do %d')).replace('%d', parseInt(item.beds, 10) || '');
2326      else if (item.type === 'budget') base = String(strings.relax_budget || nativeUiFallback('Increase budget to %s', 'Budget auf %s erhöhen', 'Zwiększ budżet do %s')).replace('%s', formatEuro(item.max_price_eur));
2327      else if (item.type === 'location') base = strings.relax_location || nativeUiFallback('Search the whole Costa del Sol', 'An der gesamten Costa del Sol suchen', 'Szukaj na całym Costa del Sol');
2328      else if (item.type === 'all_features') base = strings.relax_all_features || nativeUiFallback('Treat additional features as preferences', 'Zusätzliche Merkmale als Präferenzen behandeln', 'Traktuj dodatkowe cechy jako preferencje');
2329      else base = item.label || '';
2330      return count > 0 ? base + ' (' + count + ')' : base;
2331    }
2332
2333    function renderRelaxationSuggestions(items) {
2334      var $box = $panel.find('.voigt-ai-browser-suggestions');
2335      var $inner = $box.find('div').empty();
2336      currentSuggestions = [];
2337      (items || []).forEach(function(item){
2338        if (!item || !item.criteria) return;
2339        var label = relaxationButtonLabel(item);
2340        $('<button type="button" class="is-relaxation">')
2341          .text(label)
2342          .on('click', function(){
2343            addMessage(label, 'user');
2344            addMessage(strings.relax_apply || strings.applied_generic || '', 'ai');
2345            $inner.empty();
2346            $box.hide();
2347            addLoading(strings.searching || '');
2348            applyCriteria(item.criteria || {});
2349          })
2350          .appendTo($inner);
2351      });
2352      $box.toggle(!!$inner.children().length);
2353    }
2354
2355    function requestRelaxations(criteria) {
2356      addMessage(strings.no_results || '', 'ai');
2357      addLoading(strings.thinking || '');
2358      $.ajax({
2359        url: cfg.ajaxUrl,
2360        type: 'POST',
2361        dataType: 'json',
2362        data: {
2363          action: 'voigt_ai_browser_relaxations',
2364          nonce: cfg.nonce,
2365          lang: currentLanguage(),
2366          session_id: sessionId,
2367          session_token: sessionToken,
2368          criteria: JSON.stringify(criteria || currentCriteria())
2369        }
2370      }).done(function(response){
2371        removeLoading();
2372        var candidates = response && response.success && response.data ? (response.data.candidates || []) : [];
2373        if (candidates.length) {
2374          addMessage(strings.relax_intro || '', 'ai');
2375          renderRelaxationSuggestions(candidates);
2376        } else {
2377          addMessage(strings.relax_none || strings.no_results || '', 'ai');
2378        }
2379      }).fail(function(){
2380        removeLoading();
2381        addMessage(strings.relax_none || strings.no_results || '', 'ai');
2382      });
2383    }
2384
2385    function finishPendingSearch(payload) {
2386      payload = payload || {};
2387      if (!pendingSearch || !pendingSearchId) return;
2388      if (String(payload.request_id || '') !== String(pendingSearchId)) return;
2389
2390      var count = payload.total_count;
2391      if (count !== null && count !== undefined && count !== '') count = parseInt(count, 10);
2392      else count = null;
2393
2394      var context = pendingSearchContext || {};
2395      pendingSearch = false;
2396      pendingSearchId = '';
2397      pendingSearchContext = {silent: false, scrollToResults: false};
2398      if (pendingSearchTimer) window.clearTimeout(pendingSearchTimer);
2399      pendingSearchTimer = null;
2400      $form.find('input[name="_voigt_ai_request_id"]').remove();
2401      removeLoading();
2402
2403      if (count === 0) {
2404        // Also analyse silent searches made after navigation from another page.
2405        // The panel remains closed, but the explanation and choices are ready
2406        // when the user opens the existing conversation.
2407        requestRelaxations(window.__voigtAiLastCriteria || currentCriteria());
2408      } else if (!context.silent) {
2409        if (count !== null && isFinite(count) && count > 0) {
2410          addMessage((strings.results || '').replace('%d', count), 'ai', {
2411            cta: (strings.view_results || '').replace('%d', count)
2412          });
2413        } else {
2414          addMessage(strings.results_ready || strings.applied_generic || '', 'ai');
2415        }
2416      }
2417      refreshSummary();
2418      if (context.scrollToResults) window.setTimeout(scrollToResults, 100);
2419    }
2420
2421    function scrollToResults() {
2422      var element = getResultsRoot().get(0);
2423      if (element) element.scrollIntoView({behavior: 'smooth', block: 'start'});
2424    }
2425
2426    // Accept a result count only from the exact MLS request created by the AI.
2427    // DOM observers and previously rendered counters are intentionally ignored.
2428    $(document).on('voigt:mls:results-updated', function(event, payload){
2429      finishPendingSearch(payload || {});
2430    });
2431
2432    $(document).on('click', '[data-auth-open]', function(event){
2433      if (!cfg.authPopupEnabled || !$authModal.length) return;
2434      // A modal can originate inside a launcher marked data-auth-open before it
2435      // is moved to <body>. Never interpret clicks on fields or checkboxes in
2436      // the already-open dialog as another request to reopen and refocus it.
2437      if ($(event.target).closest('.voigt-ai-auth-modal').length) return;
2438      event.preventDefault();
2439      openAuthModal(String($(this).attr('data-auth-open') || 'login'));
2440    });
2441    $authModal.on('click', '[data-auth-close]', function(event){ event.preventDefault(); closeAuthModal(); });
2442    $authModal.on('click', '.voigt-ai-auth-form input, .voigt-ai-auth-form label', function(event){ event.stopPropagation(); });
2443    $authModal.on('click', '.voigt-ai-auth-privacy a', function(event){ event.stopPropagation(); });
2444    $authModal.on('click', '.voigt-ai-auth-tab', function(){ setAuthTab(String($(this).attr('data-auth-tab') || 'login')); });
2445    $authModal.on('click', '[data-auth-mode]', function(event){ event.preventDefault(); setAuthTab(String($(this).attr('data-auth-mode') || 'login')); });
2446    $authModal.on('submit', '.voigt-ai-auth-form', function(event){ event.preventDefault(); submitAuthForm($(this)); });
2447    // Some themes and optimisation plugins install document-level form
2448    // handlers that cancel popup submissions. Capture the primary submit-button
2449    // click before those handlers, then invoke the same validated AJAX path.
2450    var authModalNode = $authModal.get(0);
2451    if (authModalNode && !authModalNode.__voigtAuthCaptureBound) {
2452      authModalNode.__voigtAuthCaptureBound = true;
2453      authModalNode.addEventListener('click', function(event){
2454        var button = event.target && event.target.closest ? event.target.closest('.voigt-ai-auth-submit') : null;
2455        if (!button || !authModalNode.contains(button)) return;
2456        event.preventDefault();
2457        event.stopPropagation();
2458        submitAuthForm($(button).closest('.voigt-ai-auth-form'));
2459      }, true);
2460    }
2461    $(document).on('voigt:ai:open-auth', function(event, payload){
2462      var mode = payload && payload.mode ? payload.mode : 'login';
2463      openAuthModal(mode);
2464    });
2465    $(document).on('keydown.voigtAiAuth', function(event){
2466      if (event.key === 'Escape' && $authModal.length && !$authModal.prop('hidden')) closeAuthModal();
2467    });
2468
2469    $panel.on('click', '.voigt-ai-browser-close', closePanel);
2470    $panel.on('click', '.voigt-ai-card a[href], .voigt-ai-browser-property-link[href]', function(){
2471      var $link = $(this);
2472      var $card = $link.closest('.voigt-ai-card');
2473      var ref = String($card.attr('data-reference') || $card.data('reference') || $link.attr('data-reference') || '').trim();
2474      if (!ref) {
2475        var text = String($card.text() || '');
2476        var match = text.match(/\bR\s*([0-9]{4,})\b/i);
2477        if (match) ref = 'R' + match[1];
2478      }
2479      if (ref) trackAdvisorEvent('property_open', ref);
2480    });
2481
2482    $panel.on('click', '.voigt-ai-browser-handoff-close', closeHandoff);
2483    $backdrop.on('click', closePanel);
2484    $(document).on('click', '.voigt-ai-browser-launcher,.voigt-ai-browser-edit', function(){ openPanel
2484(); });
2485
2486    /* Public API so other modules (calculators, landing pages) can open the
2487       advisor and seed it with context. Additive only, nothing else changes. */
2488    window.voigtAiAdvisor = window.voigtAiAdvisor || {};
2489    window.voigtAiAdvisor.open = function(){ openPanel(); };
2490    window.voigtAiAdvisor.openAuth = function(mode){ openAuthModal(mode || 'login'); };
2491    window.voigtAiAdvisor.openHandoff = function(){ openPanel(false); openHandoff(); };
2492    window.voigtAiAdvisor.ask = function(message){
2493      if (!$.trim(message || '')) { openPanel(); return; }
2494      sendMessage(message, '');
2495    };
2496    window.voigtAiAdvisor.ready = true;
2497    try { $(document).trigger('voigt-ai-advisor-ready'); } catch (e) {}
2498    if (cfg.resetPassword && cfg.resetPassword.active) {
2499      window.setTimeout(function(){ openAuthModal('reset'); }, 80);
2500    }
2501    $panel.on('click', '.voigt-ai-browser-reset', function(){
2502      clearSession();
2503      $messages.empty();
2504      addMessage(initialGreeting(), 'ai', {small: strings.hint || ''});
2505      closeHandoff();
2506      renderSuggestions(cfg.startSuggestions || []);
2507    });
2508    $panel.find('.voigt-ai-browser-composer').on('submit', function(event){
2509      event.preventDefault();
2510      sendMessage($(this).find('textarea').val(), '');
2511    });
2512    function resizeComposerTextarea(element) {
2513      if (!element) return;
2514      element.style.height = 'auto';
2515      element.style.height = Math.min(112, Math.max(48, element.scrollHeight)) + 'px';
2516    }
2517
2518    $panel.find('.voigt-ai-browser-composer textarea')
2519      .on('input', function(){ resizeComposerTextarea(this); settleMobileViewport(); })
2520      .on('focus', function(){ resizeComposerTextarea(this); settleMobileViewport(); })
2521      .on('keydown', function(event){
2522        if (event.key === 'Enter' && !event.shiftKey) {
2523          event.preventDefault();
2524          $(this).closest('form').trigger('submit');
2525        }
2526      });
2527    $panel.on('focusin', 'textarea,input', function(){ settleMobileViewport(); });
2528    $(document).on('click.voigtAiMoreFilters', '.voigt-ai-browser-listing-host .mls-af-selected-labels .mls-af-close-btn', function(event){
2529      var $badge = $(this).closest('.mls-af-label-badge');
2530      var value = String($badge.attr('data-value') || '');
2531      if (!value) return;
2532      event.preventDefault();
2533      event.stopPropagation();
2534      setFeatureChecked(findFeatureInputs([value]), false);
2535      rebuildFeatureUi();
2536      ensureFeatureSubmissionMirrors(getFeatureInputs().filter(':checked').map(function(){ return String(this.value); }).get());
2537      refreshSummary();
2538    });
2539
2540    $(document).on('change.voigtAiMoreFilters', '.voigt-ai-browser-listing-host input.mls-af-checkbox', function(event){
2541      if (event.originalEvent && event.originalEvent.isTrusted && window.__voigtAiFeatureSync) {
2542        window.__voigtAiFeatureSync.userModified = true;
2543      }
2544      rebuildFeatureUi();
2545      ensureFeatureSubmissionMirrors(getFeatureInputs().filter(':checked').map(function(){ return String(this.value); }).get());
2546    });
2547
2548    $(document).on('click.voigtAiMoreFilters', '.voigt-ai-browser-listing-host .mls-add-feat-btn', function(){
2549      var sync = window.__voigtAiFeatureSync || {};
2550      if (!sync.userModified && Array.isArray(sync.requested) && sync.requested.length) {
2551        window.setTimeout(function(){
2552          var checkedCount = findFeatureInputs(sync.requested).filter(':checked').length;
2553          if (checkedCount < sync.requested.length) applyMoreFilters(window.__voigtAiLastCriteria || {});
2554        }, 40);
2555      }
2556    });
2557
2558    $(document).on('change input', '.mls-proplist-search-form select,.mls-proplist-search-form input', function(){
2559      if (!pendingSearch) refreshSummary();
2560    });
2561
2562    $panel.on('click', '.voigt-ai-browser-send-lead', function(){
2563      if (!ensureTurnstileReady(function(){ $panel.find('.voigt-ai-browser-send-lead').trigger('click'); })) return;
2564      var $handoff = $panel.find('.voigt-ai-browser-handoff');
2565      var $status = $handoff.find('.voigt-ai-browser-lead-status').text('');
2566      var handoffTurnstileToken = tsToken();
2567      if (turnstileRequired()) {
2568        $panel.find('input[name="cf-turnstile-response"]').val('');
2569        turnstileConsumed = true;
2570        syncTurnstileControls();
2571      }
2572      $.post(cfg.ajaxUrl, {
2573        action: 'voigt_ai_handoff',
2574        nonce: cfg.handoffNonce,
2575        session_id: sessionId,
2576        session_token: sessionToken,
2577        turnstile: handoffTurnstileToken,
2578        current_criteria: JSON.stringify(currentCriteria()),
2579        name: $handoff.find('[name="name"]').val(),
2580        email: $handoff.find('[name="email"]').val(),
2581        phone: $handoff.find('[name="phone"]').val(),
2582        consent: $handoff.find('[name="consent"]').is(':checked') ? 1 : 0
2583      }).done(function(response){
2584        tsReset();
2585        $status.text(response && response.success ? strings.lead_ok : ((response && response.data && response.data.message) || str
2585ings.error));
2586        if (response && response.success) { $handoff.find('.voigt-ai-browser-send-lead').data('voigtLeadSent', 1); $handoff.find('input,.voigt-ai-browser-send-lead').prop('disabled', true); }
2587      }).fail(function(){
2588        tsReset();
2589        $status.text(strings.error);
2590      });
2591    });
2592
2593    updateQuotaNotice(currentQuota);
2594    initialiseTurnstileGate();
2595    try {
2596      if (sessionStorage.getItem('voigtAiBrowserReopenAfterAuth') === '1') {
2597        sessionStorage.removeItem('voigtAiBrowserReopenAfterAuth');
2598        window.setTimeout(function(){ openPanel(false); }, 180);
2599      }
2600    } catch (e) {}
2601
2602    document.body.addEventListener('transitionend', function(event){
2603      if (event && event.propertyName === 'padding-left') refreshNativeListingLayout();
2604    });
2605
2606    if (window.visualViewport) {
2607      window.visualViewport.addEventListener('resize', settleMobileViewport);
2608      window.visualViewport.addEventListener('scroll', settleMobileViewport);
2609    }
2610    window.addEventListener('resize', settleMobileViewport);
2611    window.addEventListener('orientationchange', function(){ setTimeout(settleMobileViewport, 120); });
2612
2613    $(document).on('keydown', function(event){
2614      if (event.key !== 'Escape' || !$root.hasClass('is-open')) return;
2615      if (!$panel.find('.voigt-ai-browser-handoff').prop('hidden')) closeHandoff();
2616      else closePanel();
2617    });
2618
2619    var $teaser = $('.voigt-ai-browser-teaser');
2620
2621    function teaserStorageGet(key) {
2622      try { return localStorage.getItem(key) || ''; } catch (e) {}
2623      try { return sessionStorage.getItem(key) || ''; } catch (e2) {}
2624      return '';
2625    }
2626
2627    function teaserStorageSet(key, value) {
2628      var stored = false;
2629      try { localStorage.setItem(key, value); stored = true; } catch (e) {}
2630      // Keep a session fallback for privacy modes/browsers that block localStorage.
2631      if (!stored) { try { sessionStorage.setItem(key, value); } catch (e2) {} }
2632    }
2633
2634    function teaserWasSeenOrDismissed() {
2635      if (teaserStorageGet(teaserSeenKey) || teaserStorageGet(teaserDismissedKey
2635)) return true;
2636
2637      // Promote the previous session-only close marker to the new persistent,
2638      // language-independent preference so existing visitors are not prompted
2639      // again merely because this behaviour was upgraded.
2640      try {
2641        var legacyLangs = ['pl', 'en', 'es', 'de', 'fr'];
2642        for (var i = 0; i < legacyLangs.length; i++) {
2643          if (sessionStorage.getItem('voigtAiTeaserShown:' + legacyLangs[i])) {
2644            teaserStorageSet(teaserSeenKey, '1');
2645            return true;
2646          }
2647        }
2648      } catch (e) {}
2649      return false;
2650    }
2651
2652    if ($teaser.length && !teaserWasSeenOrDismissed()) {
2653      setTimeout(function(){
2654        // Re-check storage after the delay in case another tab/page has already
2655        // displayed or dismissed the hint in the meantime.
2656        if (!teaserWasSeenOrDismissed() && !restoredConversation && !$panel.hasClass('is-open')) {
2657          $teaser.prop('hidden', false);
2658          // Mark on actual display: the discovery bubble is intentionally shown
2659          // only once per browser profile, even when the visitor simply ignores it.
2660          teaserStorageSet(teaserSeenKey, '1');
2661        }
2662      }, 4000);
2663    }
2664    $(document).on('click', '.voigt-ai-browser-teaser-close', function(event){
2665      event.preventDefault();
2666      event.stopPropagation();
2667      $teaser.prop('hidden', true);
2668      teaserStorageSet(teaserSeenKey, '1');
2669      teaserStorageSet(teaserDismissedKey, '1');
2670    });
2671    $(document).on('click', '.voigt-ai-browser-teaser', function(event){
2672      if ($(event.target).closest('.voigt-ai-browser-teaser-close').length) return;
2673      $teaser.prop('hidden', true);
2674      teaserStorageSet(teaserSeenKey, '1');
2675      openPanel();
2676    });
2677
2678    $panel.on('click', '.voigt-ai-browser-contacts a[data-type="whatsapp"]', function(){
2679      var criteria = window.__voigtAiLastCriteria || currentCriteria();
2680      var labels = criteriaLabels(criteria);
2681      var brief;
2682      if (currentLanguage() === 'pl') {
2683        brief = 'Dzień dobry, szukam: ' + (labels.join(', ') || 'nieruchomości na Costa del Sol') + ' (przez ' + (cfg.advisorName || 'Voigt AI Advisor') + ').';
2684      } else if (currentLanguage() === 'de') {
2685        brief = 'Guten Tag, ich suche: ' + (labels.join(', ') || 'eine Immobilie an der Costa del Sol') + ' (über ' + (cfg.advisorName || 'Voigt AI Advisor') + ').';
2686      } else {
2687        brief = 'Hello, I am looking for: ' + (labels.join(', ') || 'a property on the Costa del Sol') + ' (via ' + (cfg.advisorName || 'Voigt AI Advisor') + ' AI).';
2688      }
2689      var base = (this.href || '').split('?')[0];
2690      this.href = base + '?text=' + encodeURIComponent(brief);
2691    });
2692
2693    try {
2694      // Consume a pending cross-page search exactly once. Only the v3 payload
2695      // written by an explicit results CTA is accepted; old v2 payloads could
2696      // be created merely by chatting and are discarded as stale automation.
2697      if (!window.__voigtAiPendingCriteriaConsumed) {
2698        window.__voigtAiPendingCriteriaConsumed = true;
2699        var savedCriteriaRaw = sessionStorage.getItem('voigtAiPendingCriteria');
2700        if (savedCriteriaRaw && getForm().length) {
2701          sessionStorage.removeItem('voigtAiPendingCriteria');
2702          var savedPayload = JSON.parse(savedCriteriaRaw);
2703          var isExplicitCta = savedPayload
2704            && Number(savedPayload.version || 0) >= 4
2705            && String(savedPayload.source || '') === 'results_cta';
2706          var criteriaToApply = isExplicitCta && savedPayload.criteria ? savedPayload.criteria : null;
2707          var createdAt = savedPayload && savedPayload.createdAt ? Number(savedPayload.createdAt) : 0;
2708          var expired = !createdAt || (Date.now() - createdAt > 2 * 60 * 1000);
2709          var hashMatch = String(window.location.hash || '').match(/^#voigt-ai-search=([^&]+)$/);
2710          var hashNavigationId = hashMatch ? decodeURIComponent(hashMatch[1] || '') : '';
2711          var navigationMatches = isExplicitCta
2712            && hashNavigationId !== ''
2713            && hashNavigationId === String(savedPayload.navigationId || '')
2714            && String(window.location.pathname || '') === String(savedPayload.targetPath || '');
2715
2716          if (hashMatch && history.replaceState) {
2717            history.replaceState(history.state, '', window.location.pathname + window.location.search);
2718          }
2719
2720          if (!expired && navigationMatches && criteriaToApply && typeof criteriaToApply === 'object') {
2721            // Apply only when the destination carries the one-time navigation
2722            // token generated by the explicit “View properties” click. A stale
2723            // sessionStorage payload can no longer trigger a later manual visit.
2724            closePanel();
2725            applyCriteria(criteriaToApply, {silent: true, scrollToResults: true});
2726          }
2727        }
2728      }
2729      sessionStorage.removeItem('voigtAiPanelClosedForNavigation');
2730    } catch (e) {
2731      try { sessionStorage.removeItem('voigtAiPendingCriteria'); } catch (ignore) {}
2732    }
2733
2734    restoredConversation = restoreConversation();
2735    if (restoredConversation) {
2736      renderSuggestions(currentSuggestions, {persist: false});
2737    } else {
2738      renderSuggestions(cfg.startSuggestions || []);
2739      restoreConversationFromServer();
2740    }
2741    saveSession();
2742    refreshSummary();
2743  });
2744})(jQuery);

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.