PageSourceSearch

https://www.cupidosshop.com/js/master.js?v=27379357

js cupidosshop.com collected 2026-09-27 02:24:43 UTC 258,930 bytes, 6,065 lines download raw bytes

1/* ==========================================================
2   master.js — catálogo + filtros (AJAX completo) + UX + shopcart
3   Requer: jQuery 1.11.x+
4   ========================================================== */
5
6/* ==========================================================
7 * TABLE OF CONTENTS
8 * ----------------------------------------------------------
9 * 0. Utilities & Language Helpers
10 * 1. Catalog State + Filter Rendering
11 * 2. Event Binding Dispatcher
12 *      2.1 Header & Navigation
13 *      2.2 Favorites
14 *      2.3 Currency Switcher
15 *      2.4 Catalog Controls & Filters
16 *      2.5 Menu Toggles & Scroll Helpers
17 *      2.6 Product Page Interactions
18 *      2.7 Cart Quantity, Discounts & Checkout
19 *      2.8 Gift Toggles, Delivery API Language & Live Search
20 *      2.9 Cart Item Removal
21 * 3. Mini-cart Hover + Cart Utilities
22 * 4. Products, Filters & Misc Utilities
23 * 5. Account / Authentication
24 * 6. Delivery / Checkout Helpers
25 * 7. Tools Popover, Search Helpers & Analytics
26 * 8. Bootstrapping
27 * ========================================================== */
28
29(function ($) {
30  if (!$) return;
31
32  /* ==========================================================
33   * GLOBAL CONFIG
34   * ----------------------------------------------------------
35   * Toggle `ajax.devMode` to choose between the development and
36   * production request handlers. Production must always resolve to
37   * `/request.php`.
38   * ========================================================== */
39  var hasWindow = typeof window !== "undefined";
40  var ajaxConfig = {
41    devMode: false,
42    prodEndpoint:
43      (hasWindow && window.CUPIDO_REQUEST_ENDPOINT) || "/request.php",
44    devEndpoint: "/request.php"
45  };
46  var REQUEST_ENDPOINT = ajaxConfig.devMode
47    ? ajaxConfig.devEndpoint
48    : ajaxConfig.prodEndpoint;
49
50  function readQueryParam(name) {
51    if (!hasWindow || !name) return null;
52    var escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
53    var pattern = new RegExp("[?&]" + escaped + "=([^&#]*)");
54    var match = pattern.exec(window.location.search || "");
55    return match ? decodeURIComponent(match[1].replace(/\+/g, " ")) : null;
56  }
57
58  var filtersDebugFeature = (function () {
59    var raw = readQueryParam("filters_debug_feature");
60    if (raw === null || raw === "") return null;
61    var num = parseInt(raw, 10);
62    return isNaN(num) ? null : num;
63  })();
64
65  var filtersDebugToken = (function () {
66    var raw = readQueryParam("filters_debug");
67    return raw === null || raw === "" ? null : raw;
68  })();
69
70  if (hasWindow) {
71    window.CUPIDO_FILTERS_DEBUG_FEATURE = filtersDebugFeature;
72  }
73
74  var FILTER_QUERY_PARAM_MAP = {
75    color: "color_filters",
76    size: "sizes_filters",
77    composition: "composition_filters",
78    flavor: "flavor_filters",
79    brand: "brand_filters",
80    feature: "caracteristicas_filters"
81  };
82
83  function getUrlSearchParams() {
84    if (!hasWindow || typeof window.URLSearchParams === "undefined") return null;
85    try {
86      return new URLSearchParams(window.location.search || "");
87    } catch (e) {
88      return null;
89    }
90  }
91
92  function parseFilterListParam(raw) {
93    if (raw === null || raw === undefined || raw === "") return [];
94    return String(raw)
95      .split(",")
96      .map(function (item) { return parseInt($.trim(item), 10); })
97      .filter(function (item, index, list) {
98        return !isNaN(item) && item > 0 && list.indexOf(item) === index;
99      })
100      .map(function (item) { return String(item); });
101  }
102
103  function parseFilterPriceParam(raw) {
104    if (raw === null || raw === undefined || raw === "") return null;
105    var parsed = parseFloat(String(raw).replace(",", "."));
106    return isFinite(parsed) ? parsed : null;
107  }
108
109  function readFiltersStateFromUrl() {
110    var params = getUrlSearchParams();
111    if (!params) return null;
112
113    var hasKnownParams = params.has("filters");
114    var state = {
115      color_filters: [],
116      sizes_filters: [],
117      composition_filters: [],
118      flavor_filters: [],
119      brand_filters: [],
120      caracteristicas_filters: [],
121      price_filters: {}
122    };
123
124    $.each(FILTER_QUERY_PARAM_MAP, function (queryKey, stateKey) {
125      if (!params.has(queryKey)) return;
126      hasKnownParams = true;
127      state[stateKey] = parseFilterListParam(params.get(queryKey));
128    });
129
130    if (params.has("price_min")) {
131      hasKnownParams = true;
132      var min = parseFilterPriceParam(params.get("price_min"));
133      if (min !== null) state.price_filters.min = min;
134    }
135    if (params.has("price_max")) {
136      hasKnownParams = true;
137      var max = parseFilterPriceParam(params.get("price_max"));
138      if (max !== null) state.price_filters.max = max;
139    }
140
141    return hasKnownParams ? state : null;
142  }
143
144  function syncFiltersStateToUrl(filters) {
145    var params = getUrlSearchParams();
146    if (!params || !(history && history.replaceState)) return;
147
148    $.each(FILTER_QUERY_PARAM_MAP, function (queryKey) {
149      params.delete(queryKey);
150    });
151    params.delete("filters");
152    params.delete("price_min");
153    params.delete("price_max");
154
155    if (hasSessionFilters(filters)) {
156      params.set("filters", "1");
157
158      $.each(FILTER_QUERY_PARAM_MAP, function (queryKey, stateKey) {
159        var list = [];
160        var raw = filters && filters[stateKey];
161        if (Array.isArray(raw)) {
162          list = raw;
163        } else if (raw && typeof raw === "object") {
164          list = Object.keys(raw).map(function (key) { return raw[key]; });
165        }
166        list = parseFilterListParam((list || []).join(","));
167        if (list.length) {
168          params.set(queryKey, list.join(","));
169        }
170      });
171
172      var priceFilters = filters && filters.price_filters ? filters.price_filters : null;
173      var min = priceFilters ? parseFilterPriceParam(priceFilters.min) : null;
174      var max = priceFilters ? parseFilterPriceParam(priceFilters.max) : null;
175      if (min !== null) params.set("price_min", String(min));
176      if (max !== null) params.set("price_max", String(max));
177    }
178
179    var nextSearch = params.toString();
180    var nextUrl = window.location.pathname + (nextSearch ? "?" + nextSearch : "") + window.location.hash;
181    history.replaceState(history.state || {}, "", nextUrl);
182  }
183
184  /* ==========================================================
185   * 0) HELPERS GERAIS
186   * ========================================================== */
187  function postHTML(action, data, ok) {
188    return $.ajax({
189      type: "POST", url: REQUEST_ENDPOINT, dataType: "html", cache: false,
190      data: $.extend({ ajax: action }, data || {}), success: ok
191    });
192  }
193  function postJSON(action, data, ok) {
194    // Algumas ações não devolvem JSON válido
195    var dt = (action === "updateFilters" || action === "restoreFilters") ? "text" : "json";
196    return $.ajax({
197      type: "POST", url: REQUEST_ENDPOINT, dataType: dt, cache: false,
198      data: $.extend({ ajax: action }, data || {}), success: ok
199    });
200  }
201  var FILTER_PERSIST_DEBUG = true;
202  var fpLog = function () {
203    if (!FILTER_PERSIST_DEBUG || !console || !console.log) return;
204    var args = Array.prototype.slice.call(arguments);
205    args.unshift("[filters-persist]");
206    console.log.apply(console, args);
207  };
208  var gv = s => { var $e = $(s); return $e.length ? ($e.val() || "") : ""; };
209  var sv = (s, v) => { var $e = $(s); if ($e.length) $e.val(v); };
210
211	function currentLangCode(){
212		var fromInput = (document.getElementById('session_lang') || {}).value;
213		var fromUrl   = (location.pathname.match(/^\/(pt|en|es)(?:\/|$)/i) || [,''])[1];
214		var fromHtml  = (document.documentElement.getAttribute('lang') || '').toLowerCase();
215		var code = (fromInput || fromUrl || fromHtml || 'pt').toLowerCase();
216		if (!/^(pt|en|es)$/.test(code)) code = 'pt';
217		return code;
218	}
219
220	function gl(key){
221		if (!key) return '';
222		var pageLang = currentLangCode();
223		var dict;
224		try { dict = JSON.parse(localStorage.getItem('langObj') || 'null'); } catch(e){ dict = null; }
225
226		if (dict && dict[key] != null && (!dict.code || String(dict.code).toLowerCase() === pageLang)) {
227			return String(dict[key]);
228		}
229
230		var FB = {
231			pt: {
232				ITEM: ' item',
233				ITEMS: ' itens',
234				swal_attention: 'Atenção',
235				swal_missing_fields: 'Por favor verifique se preencheu todos os campos!',
236				swal_login_failed: 'Login falhou',
237				swal_btnok: 'OK',
238				swal_btncancel: 'Cancelar',
239				swal_btnyes: 'Sim',
240				QTD: 'un.',
241				SHOPCART_REMOVE_MODAL_UNIT: 'un.',
242				SHOPCART_DRAWER_EYEBROW: 'Cesto de compras',
243				SHOPCART_DRAWER_TITLE: 'Cesto',
244				SHOPCART_DRAWER_CLOSE: 'Fechar',
245				SHOPCART_DRAWER_SUBTOTAL: 'Subtotal',
246				SHOPCART_DRAWER_SHIPPING: 'Portes',
247				SHOPCART_DRAWER_SHIPPING_PENDING: 'A calcular',
248				SHOPCART_DRAWER_SAVINGS: 'Descontos',
249				SHOPCART_DRAWER_TOTAL: 'Total',
250				SHOPCART_DRAWER_CHECKOUT: 'FINALIZAR COMPRA',
251				SHOPCART_DRAWER_CART: 'IR PARA O CESTO',
252				SHOPCART_REMOVE_MODAL_TITLE: 'Remover do cesto?',
253				SHOPCART_SAVE_FOR_LATER_MESSAGE: 'Guarde-o nos favoritos para o encontrar mais tarde.',
254				SHOPCART_SAVE_FOR_LATER: 'Guardar para depois',
255				SHOPCART_SAVED_FOR_LATER_ADD_ALL: 'Adicionar tudo ao cesto',
256				SHOPCART_SAVED_FOR_LATER_LIST: 'Guardados para mais tarde',
257				SHOPCART_REMOVE: 'Remover',
258				SHOPCART_ADDED_OK: 'Adicionado ao carrinho',
259				CESTO_VAZIO1: 'O cesto está vazio',
260				CESTO_VAZIO2: 'Vê as novidades da loja.',
261				CESTO_VAZIO3: 'Ver produtos',
262				CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TITLE: 'Listas e Favoritos',
263				CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TEXT: 'Para usar a funcionalidade de Listas e Favoritos, por favor faça login ou registe-se',
264				CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_REGISTER: 'Registar',
265				CV2_STOCK_LAST_UNIT: 'Última unidade disponível',
266				CV2_STOCK_LAST_HINT: 'Outro cliente pode concluir a compra primeiro',
267				CV2_STOCK_LOW: 'Só restam {N} unidades',
268				CV2_STOCK_SOLDOUT_BADGE: 'Esgotado',
269				CV2_STOCK_SOLDOUT_MSG: 'Este artigo ficou sem stock',
270				CV2_STOCK_UPDATED: 'Stock atualizado',
271				CV2_STOCK_ADJUST: 'Apenas {N} unidades disponíveis',
272				CV2_STOCK_ADJUST_BTN: 'Ajustar para {N}',
273				CV2_STOCK_NOTIFY: 'Avisar-me quando disponível',
274				CV2_STOCK_REMOVE: 'Remover do cesto',
275				CV2_STOCK_BLOCK_MSG: 'Um artigo do seu cesto ficou indisponível',
276				CV2_STOCK_BLOCK_CTA: 'Remover indisponíveis e continuar',
277				CV2_SHIPPING_ADDRESS_REQUIRED: 'Indique a morada de entrega completa (nome, morada, código postal, localidade e país) antes de pagar.',
278				CV2_SHIPPING_PHONE_REQUIRED: 'Indique um telemóvel válido (9 a 15 dígitos) antes de pagar.'
279			},
280			en: {
281				ITEM: ' item',
282				ITEMS: ' items',
283				swal_attention: 'Attention',
284				swal_missing_fields: 'Please verify if you have completed all the required fields!',
285				swal_login_failed: 'Login failed',
286				swal_btnok: 'OK',
287				swal_btncancel: 'Cancel',
288				swal_btnyes: 'Yes',
289				QTD: 'un.',
290				SHOPCART_REMOVE_MODAL_UNIT: 'un.',
291				SHOPCART_DRAWER_EYEBROW: 'Shopping bag',
292				SHOPCART_DRAWER_TITLE: 'Bag',
293				SHOPCART_DRAWER_CLOSE: 'Close',
294				SHOPCART_DRAWER_SUBTOTAL: 'Subtotal',
295				SHOPCART_DRAWER_SHIPPING: 'Shipping',
296				SHOPCART_DRAWER_SHIPPING_PENDING: 'Calculating',
297				SHOPCART_DRAWER_SAVINGS: 'Discounts',
298				SHOPCART_DRAWER_TOTAL: 'Total',
299				SHOPCART_DRAWER_CHECKOUT: 'CHECKOUT',
300				SHOPCART_DRAWER_CART: 'GO TO BAG',
301				SHOPCART_REMOVE_MODAL_TITLE: 'Remove from bag?',
302				SHOPCART_SAVE_FOR_LATER_MESSAGE: 'Save it to find it later.',
303				SHOPCART_SAVE_FOR_LATER: 'Save for later',
304				SHOPCART_SAVED_FOR_LATER_ADD_ALL: 'Add all to bag',
305				SHOPCART_SAVED_FOR_LATER_LIST: 'Saved for later',
306				SHOPCART_REMOVE: 'Remove',
307				SHOPCART_ADDED_OK: 'Added to bag',
308				CESTO_VAZIO1: 'Your bag is empty',
309				CESTO_VAZIO2: "See what's new in the shop.",
310				CESTO_VAZIO3: 'See products',
311				CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TITLE: 'Lists and Favourites',
312				CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TEXT: 'To use Lists and Favourites, please sign in or register',
313				CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_REGISTER: 'Register',
314				CV2_STOCK_LAST_UNIT: 'Last unit available',
315				CV2_STOCK_LAST_HINT: 'Another customer may complete the purchase first',
316				CV2_STOCK_LOW: 'Only {N} units left',
317				CV2_STOCK_SOLDOUT_BADGE: 'Sold out',
318				CV2_STOCK_SOLDOUT_MSG: 'This item is out of stock',
319				CV2_STOCK_UPDATED: 'Stock updated',
320				CV2_STOCK_ADJUST: 'Only {N} units available',
321				CV2_STOCK_ADJUST_BTN: 'Adjust to {N}',
322				CV2_STOCK_NOTIFY: 'Notify me when available',
323				CV2_STOCK_REMOVE: 'Remove from cart',
324				CV2_STOCK_BLOCK_MSG: 'An item in your cart became unavailable',
325				CV2_STOCK_BLOCK_CTA: 'Remove unavailable and continue',
326				CV2_SHIPPING_ADDRESS_REQUIRED: 'Please enter a complete delivery address (name, address, postal code, city and country) before paying.',
327				CV2_SHIPPING_PHONE_REQUIRED: 'Please enter a valid mobile number (9 to 15 digits) before paying.'
328			},
329			es: {
330				ITEM: ' artículo',
331				ITEMS: ' artículos',
332				swal_attention: 'Atención',
333				swal_missing_fields: 'Por favor, compruebe que ha llenado todos los campos!',
334				swal_login_failed: 'Login fallido',
335				swal_btnok: 'OK',
336				swal_btncancel: 'Cancelar',
337				swal_btnyes: 'Sí',
338				QTD: 'ud.',
339				SHOPCART_REMOVE_MODAL_UNIT: 'ud.',
340				SHOPCART_DRAWER_EYEBROW: 'Cesta de compras',
341				SHOPCART_DRAWER_TITLE: 'Cesta',
342				SHOPCART_DRAWER_CLOSE: 'Cerrar',
343				SHOPCART_DRAWER_SUBTOTAL: 'Subtotal',
344				SHOPCART_DRAWER_SHIPPING: 'Envío',
345				SHOPCART_DRAWER_SHIPPING_PENDING: 'Calculando',
346				SHOPCART_DRAWER_SAVINGS: 'Descuentos',
347				SHOPCART_DRAWER_TOTAL: 'Total',
348				SHOPCART_DRAWER_CHECKOUT: 'FINALIZAR COMPRA',
349				SHOPCART_DRAWER_CART: 'IR A LA CESTA',
350				SHOPCART_REMOVE_MODAL_TITLE: '¿Quitar de la cesta?',
351				SHOPCART_SAVE_FOR_LATER_MESSAGE: 'Guárdalo para encontrarlo más tarde.',
352				SHOPCART_SAVE_FOR_LATER: 'Guardar para más tarde',
353				SHOPCART_SAVED_FOR_LATER_ADD_ALL: 'Añadir todo a la cesta',
354				SHOPCART_SAVED_FOR_LATER_LIST: 'Guardados para más tarde',
355				SHOPCART_REMOVE: 'Eliminar',
356				SHOPCART_ADDED_OK: 'Añadido a la cesta',
357				CESTO_VAZIO1: 'La cesta está vacía',
358				CESTO_VAZIO2: 'Mira las novedades de la tienda.',
359				CESTO_VAZIO3: 'Ver productos',
360				CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TITLE: 'Listas y Favoritos',
361				CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TEXT: 'Para usar Listas y Favoritos, inicie sesión o regístrese',
362				CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_REGISTER: 'Registrarse',
363				CV2_STOCK_LAST_UNIT: 'Última unidad disponible',
364				CV2_STOCK_LAST_HINT: 'Otro cliente puede completar la compra primero',
365				CV2_STOCK_LOW: 'Solo quedan {N} unidades',
366				CV2_STOCK_SOLDOUT_BADGE: 'Agotado',
367				CV2_STOCK_SOLDOUT_MSG: 'Este artículo se quedó sin stock',
368				CV2_STOCK_UPDATED: 'Stock actualizado',
369				CV2_STOCK_ADJUST: 'Solo {N} unidades disponibles',
370				CV2_STOCK_ADJUST_BTN: 'Ajustar a {N}',
371				CV2_STOCK_NOTIFY: 'Avisarme cuando esté disponible',
372				CV2_STOCK_REMOVE: 'Quitar del carrito',
373				CV2_STOCK_BLOCK_MSG: 'Un artículo de su cesta quedó no disponible',
374				CV2_STOCK_BLOCK_CTA: 'Quitar no disponibles y continuar',
375				CV2_SHIPPING_ADDRESS_REQUIRED: 'Indique la dirección de entrega completa (nombre, dirección, código postal, localidad y país) antes de pagar.',
376				CV2_SHIPPING_PHONE_REQUIRED: 'Indique un móvil válido (9 a 15 dígitos) antes de pagar.'
377			}
378		};
379		var pack = FB[pageLang] || FB.pt;
380		if (pack[key] != null) return pack[key];
381		if (FB.pt[key] != null) return FB.pt[key];
382		return key;
383	}
384	window.gl = gl;
385
386	// codigo de desconto partilhado entre sidebar e checkout (persistido no browser;
387	// sem isto, o populate de uma pagina com input vazio apagava o desconto da outra)
388	function cv2GetDiscount(){ try { return localStorage.getItem("cv2_discount_code") || ""; } catch (e) { return ""; } }
389	function cv2SetDiscount(c){ try { if (c) localStorage.setItem("cv2_discount_code", c); else localStorage.removeItem("cv2_discount_code"); } catch (e) {} }
390	window.cv2GetDiscount = cv2GetDiscount;
391	window.cv2SetDiscount = cv2SetDiscount;
392
393	// pais de entrega escolhido (guest) partilhado entre sidebar e checkout
394	function cv2GetCountry(){ try { return localStorage.getItem("cv2_country_id") || ""; } catch (e) { return ""; } }
395	function cv2SetCountry(c){ try { if (c) localStorage.setItem("cv2_country_id", String(c)); else localStorage.removeItem("cv2_country_id"); } catch (e) {} }
396	window.cv2GetCountry = cv2GetCountry;
397	window.cv2SetCountry = cv2SetCountry;
398
399	// Mantém apenas meta aqui – NÃO tocar em 'langObj'
400	function ensureLangMeta() {
401		var fromInput = (document.getElementById('session_lang') || {}).value;
402		var fromUrl   = (location.pathname.match(/^\/(pt|en|es)(?:\/|$)/i) || [,''])[1];
403		var fromHtml  = (document.documentElement.getAttribute('lang') || '').toLowerCase();
404		var code = (fromInput || fromUrl || fromHtml || 'pt').toLowerCase();
405		if (!/^(pt|en|es)$/.test(code)) code = 'pt';
406
407		var meta = { code, clang_id: code, locale: code==='pt'?'pt-PT':code==='es'?'es-ES':'en-GB', currency: 'EUR', symbol: '€', ts: Date.now() };
408		try { localStorage.setItem('langMeta', JSON.stringify(meta)); } catch(e){}
409		if (window.Cookies) Cookies.set('clang_id', code, { expires: 365, path: '/' });
410		return meta;
411	}
412
413
414	function fetchLangDict() {
415			var meta;
416			try { meta = JSON.parse(localStorage.getItem('langMeta') || 'null'); } catch(e){}
417			var lang = (meta && meta.code) || 'pt';
418
419			return $.ajax({
420				type: "POST",
421				url:  REQUEST_ENDPOINT,
422				data: { ajax: "getLanguageObj", lang_id: lang },
423				dataType: "json",        // <- importante!
424				cache: false
425			}).done(function(dict){
426				try {
427					var merged = {};
428					if (meta && typeof meta === 'object') merged = $.extend({}, meta);
429					if (dict && typeof dict === 'object') merged = $.extend(merged, dict);
430					if (!merged.code) merged.code = lang;
431					localStorage.setItem('langObj', JSON.stringify(merged));
432				} catch(e){}
433			});
434		}
435
436  /* ==========================================================
437   * 1) COOKIES + LÍNGUA
438   * ========================================================== */
439  // Fallback caso js-cookie não esteja carregado
440  (function (w) {
441    if (!w.Cookies) {
442      w.Cookies = {
443        get: function (name) {
444          var m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[\]\\/+^])/g, '\\$1') + '=([^;]*)'));
445          return m ? decodeURIComponent(m[1]) : undefined;
446        },
447        set: function (name, value, opts) {
448          opts = opts || {};
449          var str = name + '=' + encodeURIComponent(value);
450          if (opts.expires) {
451            var d = new Date();
452            d.setTime(d.getTime() + (opts.expires * 864e5)); // dias
453            str += '; expires=' + d.toUTCString();
454          }
455          str += '; path=' + (opts.path || '/');
456          document.cookie = str;
457        }
458      };
459    }
460  })(window);
461
462  // Sincroniza cookie clang_id + localStorage langObj
463  function resetLangObj() {
464    var fromInput = (document.getElementById('session_lang') || {}).value;
465    var fromUrl   = (location.pathname.match(/^\/(pt|en|es)(?:\/|$)/i) || [,''])[1];
466    var fromHtml  = (document.documentElement.getAttribute('lang') || '').toLowerCase();
467    var code = (fromInput || fromUrl || fromHtml || 'pt').toLowerCase();
468    if (!/^(pt|en|es)$/.test(code)) code = 'pt';
469
470    var obj = {
471      code: code,
472      clang_id: code,                                 // compat c
472om legado
473      locale: code === 'pt' ? 'pt-PT' : code === 'es' ? 'es-ES' : 'en-GB',
474      currency: 'EUR',
475      symbol: '€',
476      ts: Date.now()
477    };
478
479    try {
480      localStorage.setItem('langMeta', JSON.stringify(obj));
481      var existingLangObj = JSON.parse(localStorage.getItem('langObj') || 'null');
482      if (!existingLangObj || typeof existingLangObj !== 'object') existingLangObj = {};
483      existingLangObj.code = obj.code;
484      existingLangObj.clang_id = obj.clang_id;
485      existingLangObj.locale = obj.locale;
486      existingLangObj.currency = obj.currency;
487      existingLangObj.symbol = obj.symbol;
488      existingLangObj.ts = obj.ts;
489      localStorage.setItem('langObj', JSON.stringify(existingLangObj));
490    } catch (e) {}
491    Cookies.set('clang_id', code, { expires: 365, path: '/' });
492    return obj;
493  }
494
495  // Garante que existe langObj + clang_id
496  (function ensureLang() {
497    var clang_id = Cookies.get('clang_id');
498    var langObj;
499    try { langObj = JSON.parse(localStorage.getItem('langObj') || 'null'); } catch(e){ langObj = null; }
500    if (!clang_id || !langObj || !langObj.code) resetLangObj();
501  })();
502
503  /* ==========================================================
504   * 2) ESTADO DO CATÁLOGO
505   * ========================================================== */
506  var Cat = { order: "", perPage: "32", page: "1" };
507  var lastCatalogResultCount = 0;
508  var FILTER_CATEGORY_COOKIE = "catalog_filters_category";
509  var FILTER_CATEGORY_STORAGE = "catalog_filters_last_cat";
510  var FILTERS_STORAGE_PREFIX = "catalog_filters_";
511  var FILTER_CATEGORY_FALLBACK = "catalog_filters_last_cat_cookie";
512  var shouldResetFiltersOnBoot = false;
513
514  function getSavedFilterCategory() {
515    var stored = null;
516    try { stored = localStorage.getItem(FILTER_CATEGORY_STORAGE); } catch (e) { stored = null; }
517    if (!stored) stored = Cookies.get(FILTER_CATEGORY_COOKIE) || Cookies.get(FILTER_CATEGORY_FALLBACK) || null;
518    return stored;
519  }
520
521  function rememberFilterCategory(catId) {
522    if (!catId) return;
523    Cookies.set(FILTER_CATEGORY_COOKIE, String(catId), { expires: 30, path: "/" });
524    Cookies.set(FILTER_CATEGORY_FALLBACK, String(catId), { expires: 30, path: "/" });
525    try { localStorage.setItem(FILTER_CATEGORY_STORAGE, String(catId)); } catch (e) {}
526  }
527
528  function getFilterCategoryKey() {
529    return gv("#extra_query_filters") || gv("#extra_query") || "";
530  }
531
532  function saveFiltersSnapshot(catId, filters) {
533    if (!catId) return;
534    var safe = filters || {};
535    var normalizeList = function (v) {
536      if (Array.isArray(v)) return v;
537      if (v && typeof v === "object") return Object.keys(v).map(function (k) { return v[k]; });
538      return [];
539    };
540    safe = {
541      color_filters: normalizeList(safe.color_filters),
542      sizes_filters: normalizeList(safe.sizes_filters),
543      composition_filters: normalizeList(safe.composition_filters),
544      flavor_filters: normalizeList(safe.flavor_filters),
545      brand_filters: normalizeList(safe.brand_filters),
546      caracteristicas_filters: normalizeList(safe.caracteristicas_filters),
547      price_filters: safe.price_filters || {}
548    };
549    try {
550      localStorage.setItem(FILTERS_STORAGE_PREFIX + catId, JSON.stringify(safe));
551      fpLog("saved snapshot", { cat: catId, safe: safe });
552    } catch (e) {}
553  }
554
555  function loadFiltersSnapshot(catId) {
556    if (!catId) return null;
557    try {
558      var raw = localStorage.getItem(FILTERS_STORAGE_PREFIX + catId);
559      var snap = raw ? JSON.parse(raw) : null;
560      fpLog("loaded snapshot", { cat: catId, snapshot: snap });
561      return snap;
562    } catch (e) {
563      fpLog("error loading snapshot", e);
564      return null;
565    }
566  }
567
568  function buildFilterTasksFromSnapshot(catId, snapshot) {
569    var tasks = [];
570    var basePayload = payload();
571    var toList = function (v) {
572      if (Array.isArray(v)) return v;
573      if (v && typeof v === "object") return Object.keys(v).map(function (k) { return v[k]; });
574      return [];
575    };
576
577    // reset first
578    tasks.push(function () {
579      fpLog("task: reset filters", { cat: catId });
580      return postJSON("updateFilters", { value: valueMap(basePayload, { reset_all: 1 }) });
581    });
582
583    function pushToggleTasks(key, list) {
584      if (!Array.isArray(list)) return;
585      list.forEach(function (val) {
586        if (!val) return;
587        tasks.push(function () {
588          var extras = { nowpage: 1 };
589          extras[key] = val;
590          return postJSON("updateFilters", { value: valueMap(basePayload, extras) });
591        });
592      });
593    }
594
595    pushToggleTasks("color", toList(snapshot.color_filters));
596    pushToggleTasks("size", toList(snapshot.sizes_filters));
597    pushToggleTasks("composition", toList(snapshot.composition_filters));
598    pushToggleTasks("flavor", toList(snapshot.flavor_filters));
599    pushToggleTasks("brand", toList(snapshot.brand_filters));
600    pushToggleTasks("feature", toList(snapshot.caracteristicas_filters));
601
602    if (snapshot.price_filters && (snapshot.price_filters.min || snapshot.price_filters.max)) {
603      tasks.push(function () {
604        var min = snapshot.price_filters.min;
605        var max = snapshot.price_filters.max;
606        var payloadVals = {};
607        if (min !== undefined && min !== null && min !== "") payloadVals.price_min = parseFloat(min);
608        if (max !== undefined && max !== null && max !== "") payloadVals.price_max = parseFloat(max);
609        var payload = { value: valueMap(basePayload, payloadVals) };
610        fpLog("task: price", payload);
611        return postJSON("updateFilters", payload);
612      });
613    }
614
615    fpLog("built tasks", { cat: catId, total: tasks.length });
616    return tasks;
617  }
618
619  function runTasksSequentially(tasks, done) {
620    var idx = 0;
621    var next = function () {
622      if (!tasks.length) {
623        fpLog("tasks finished");
624        if (typeof done === "function") done();
625        return;
626      }
627      var task = tasks.shift();
628      fpLog("task start", { index: idx });
629      idx += 1;
630      var req = task();
631      if (req && typeof req.always === "function") {
632        req.always(next);
633      } else {
634        next();
635      }
636    };
637    next();
638  }
639
640  function initState() {
641    var urlPage = (location.pathname.match(/\/page\/(\d+)\//) || [null, null])[1];
642    Cat.order   = gv("#order")        || $(".webstore-order:first").val()        || "";
643    Cat.perPage = gv("#espaco_lista") || $(".webstore-results-num:first").val()  || "32";
644    Cat.page    = gv("#nowpage")      || urlPage || $(".webstore-direct-page:first").val() || $(".webstore-page-num:first").val() || "1";
645    sv("#order", Cat.order); sv("#espaco_lista", Cat.perPage); sv("#nowpage", Cat.page);
646  }
647  function payload() {
648    var base = {
649      espaco_lista: Cat.perPage, nowpage: Cat.page,
650      extra_query: gv("#extra_query"),
651      extra_query_marca: gv("#extra_query_marca") || (location.pathname.match(/\/brand\/(\d+)\//) || [null, ""])[1],
652      extra_query_tipo: gv("#extra_query_tipo"),
653      novidades: gv("#novidades"),
654      search: gv("#searchq")
655    };
656    if (Cat.order) base.order = Cat.order;
657    if (filtersDebugFeature !== null) {
658      base.filters_debug_feature = filtersDebugFeature;
659    }
660    if (filtersDebugToken !== null) {
661      base.filters_debug = filtersDebugToken;
662    }
663    return base;
664  }
665  function valueMap(p, extra) {
666    var v = { nowpage: p.nowpage, espaco_lista: p.espaco_lista };
667    if (p.order) v.order = p.order;
668    if (p.extra_query) v.extra_query = p.extra_query;
669    if (p.extra_query_marca) v.extra_query_marca = p.extra_query_marca;
670    if (p.extra_query_tipo) v.extra_query_tipo = p.extra_query_tipo;
671    if (p.novidades) v.novidades = p.novidades;
672    if (p.search) v.search = p.search;
673    if (p.filters_debug_feature !== null && p.filters_debug_feature !== undefined) {
674      v.filters_debug_feature = p.filters_debug_feature;
675    }
676    if (p.filters_debug !== null && p.filters_debug !== undefined) {
677      v.filters_debug = p.filters_debug;
678    }
679    return $.extend(v, extra || {});
680  }
681
682  /* ==========================================================
683   * 3) URL /page/N/
684   * ========================================================== */
685  function pushPage(n) {
686    if (!(history && history.pushState)) return;
687    var path = location.pathname;
688    path = path.match(/\/page\/\d+\//)
689      ? path.replace(/\/page\/\d+\//, "/page/" + n + "/")
690      : path.replace(/\/?$/, "/") + "page/" + n + "/";
691    history.pushState({ page: n }, "", path + location.search + location.hash);
692  }
693
694  /* ==========================================================
695   * 4) RENDERIZAÇÃO (filtros + catálogo)
696   * ========================================================== */
697  function openSelected($root) {
698    if (!$root || !$root.length) return;
699    $root.find(".filter-table").each(function () {
700      var $table = $(this);
701      var hasActive = $table.find(".fa-selected, .is-selected, [aria-pressed='true'], :checked").length > 0;
702      var pref = readFilterAccordionPreference($table);
703      if (pref === true) {
704        setFilterAccordionState($table, true);
705      } else if (pref === false) {
706        setFilterAccordionState($table, false);
707      } else if (hasActive) {
708        setFilterAccordionState($table, true);
709      }
710    });
711  }
712
713  function buildMobileFiltersMarkup(content) {
714    var clearLabel = gl("FILTER_CLEAR") || "Limpar filtros";
715    var applyLabel = gl("FILTER_APPLY") || "Aplicar";
716    var resultsLabel = gl("FILTER_RESULTS") || "resultados";
717    var closeLabel = gl("swal_btncancel") || "Fechar";
718
719    return (
720      "<div class='mobile-filters-header'>" +
721        "<button type='button' class='mobile-filters-reset' disabled>" + clearLabel + "</button>" +
722        "<button type='button' class='mobile-filters-close' aria-label='" + closeLabel + "'>" +
723          "<i class='fa fa-times-circle fa-2x'></i>" +
724        "</button>" +
725      "</div>" +
726      "<div class='mobile-filters-content'>" + content + "</div>" +
727      "<div class='mobile-filters-footer'>" +
728        "<button type='button' class='mobile-filters-apply'>" +
729          applyLabel + " (<span class='mobile-filters-result-count'>0</span> " + resultsLabel + ")" +
730        "</button>" +
731      "</div>"
732    );
733  }
734
735  function getFiltersPanelTitle() {
736    var meta;
737    var lang = "pt";
738    try { meta = JSON.parse(localStorage.getItem("langMeta") || "null"); } catch (e) { meta = null; }
739    if (meta && meta.code) {
740      lang = meta.code.toLowerCase();
741    } else if (window.Cookies && typeof Cookies.get === "function") {
742      lang = (Cookies.get("clang_id") || "pt").toLowerCase();
743    }
744    var dictTitle = gl("FILTERS_TITLE");
745    if (dictTitle && typeof dictTitle === "string") {
746      return dictTitle;
747    }
748    switch (lang) {
749      case "en": return "Filters";
750      case "es": return "Filtros";
751      default: return "Filtros";
752    }
753  }
754
755  function buildDesktopFiltersMarkup(content) {
756    var clearLabel = gl("FILTER_CLEAR") || "Limpar filtros";
757    var title = getFiltersPanelTitle();
758    var promoHtml = "";
759    var filterHtml = content;
760
761    if (content && content.indexOf("catalog-promo-filter-card") !== -1) {
762      var $tmp = $("<div>").html(content);
763      var $promo = $tmp.find(".catalog-promo-filter-card").first();
764      if ($promo.length) {
765        promoHtml = $("<div>").append($promo.detach()).html();
766        filterHtml = $tmp.html();
767      }
768    }
769
770    return (
771      "<div class='filters-panel' data-filters-shell='desktop'>" +
772        "<div class='filters-panel-header'>" +
773          "<span class='filters-panel-title'><i class='fa fa-filter' aria-hidden='true'></i> " + title + "</span>" +
774          "<button type='button' class='filters-panel-reset' aria-label='" + clearLabel + "'>" +
775            "<span>" + clearLabel + "</span>" +
776          "</button>" +
777        "</div>" +
778        "<div class='filters-panel-content'>" + filterHtml + "</div>" +
779      "</div>" +
780      promoHtml
781    );
782  }
783
784  function updateDesktopFiltersReset(hasFilters) {
785    var $btn = $(".filters-panel-reset");
786    if (!$btn.length) return;
787    var active = !!hasFilters;
788    $btn.toggleClass("is-active", active).prop("disabled", !active);
789  }
790
791  function updateMobileFiltersReset(hasFilters) {
792    var $btn = $(".mobile-filters-reset");
793    if (!$btn.length) return;
794    var active = !!hasFilters;
795    $btn.toggleClass("is-active", active).prop("disabled", !active);
796  }
797
798  function prepareMobileFiltersState($root) {
799    if (!$root || !$root.length) return;
800    $root.find(".filter-table").each(function () {
801      var $table = $(this);
802      var hasActive = $table.find(".fa-selected, .is-selected, [aria-pressed='true'], :checked").length > 0;
803      var $header = findFilterHeader($table);
804      var target = $header.length ? $header : $table;
805      var pref = readFilterAccordionPreference($table);
806      var shouldOpen = (pref === null) ? hasActive : pref;
807      setFilterAccordionState(target, shouldOpen);
808    });
809    syncSizeFilterGrid($root);
810    syncColorFilterGrid($root);
811  }
812
813  function syncSizeFilterGrid($root) {
814    if (!$root || !$root.length) return;
815    $root.find("#size-filter-table").each(function () {
816      $(this)
817        .find("a.filtersize")
818        .each(function () {
819          var $link = $(this);
820          var isSelected = $link.find(".fa").hasClass("fa-selected");
821          $link.toggleClass("size-grid-selected", isSelected);
822        });
823    });
824  }
825
826  function syncColorFilterGrid($root) {
827    if (!$root || !$root.length) return;
828    $root.find("#color-filter-table").each(function () {
829      $(this)
830        .find("a.filtercolor")
831        .each(function () {
832          var $link = $(this);
833          var isSelected = $link.find(".fa").hasClass("fa-selected");
834          $link.toggleClass("color-grid-selected", isSelected);
835        });
836    });
837  }
838
839  function initPriceFilterTables($root) {
840    if (!$root || !$root.length) return;
841    $root.find(".price-filter-table").each(function () {
842      var $table = $(this);
843      var bounds = getPriceFilterBounds($table);
844      var minVal = parsePriceValue($table.find(".price-filter-input-min").val());
845      var maxVal = parsePriceValue($table.find(".price-filter-input-max").val());
846      var range = normalizePriceRange(minVal, maxVal, bounds);
847      syncPriceFilterFields($table, range);
848    });
849  }
850
851  function getPriceFilterBounds($table) {
852    if (!$table || !$table.length) return { min: 0, max: 0 };
853    var min = parseFloat($table.data("priceMin"));
854    var max = parseFloat($table.data("priceMax"));
855    if (!isFinite(min)) min = 0;
856    if (!isFinite(max)) max = min;
857    if (max < min) max = min;
858    return { min: min, max: max };
859  }
860
861  function normalizePriceRange(min, max, bounds, changedKey) {
862    if (!isFinite(min)) min = bounds.min;
863    if (!isFinite(max)) max = bounds.max;
864    if (min < bounds.min) min = bounds.min;
865    if (max > bounds.max) max = bounds.max;
866    if (min > max) {
867      if (changedKey === "min") {
868        min = max;
869      } else {
870        max = min;
871      }
872    }
873    return { min: min, max: max };
874  }
875
876  function parsePriceValue(val) {
877    if (typeof val === "string") {
878      val = val.replace(",", ".");
879    }
880    var num = parseFloat(val);
881    return isFinite(num) ? num : NaN;
882  }
883
884  function updatePriceTrack($table, range) {
885    if (!$table || !$table.length || !range) return;
886    var bounds = getPriceFilterBounds($table);
887    var $fill = $table.find(".price-filter-track-fill");
888    if (!$fill.length) return;
889    var minPerc = bounds.max === bounds.min ? 0 : ((range.min - bounds.min) / (bounds.max - bounds.min)) * 100;
890    var maxPerc = bounds.max === bounds.min ? 100 : ((range.max - bounds.min) / (bounds.max - bounds.min)) * 100;
891    if (!isFinite(minPerc)) minPerc = 0;
892    if (!isFinite(maxPerc)) maxPerc = 0;
893    if (minPerc < 0) minPerc = 0;
894    if (maxPerc > 100) maxPerc = 100;
895    if (maxPerc < minPerc) maxPerc = minPerc;
896    $fill.css({
897      left: minPerc + "%",
898      width: (maxPerc - minPerc) + "%"
899    });
900  }
901
902  function syncPriceFilterFields($table, range) {
903    if (!$table || !$table.length || !range) return;
904    $table.find(".price-filter-range-min").val(range.min);
905    $table.find(".price-filter-range-max").val(range.max);
906    $table.find(".price-filter-input-min").val(range.min);
907    $table.find(".price-filter-input-max").val(range.max);
908    updatePriceTrack($table, range);
909  }
910
911  function sendPriceFilterRequest(range) {
912    Cat.page = "1"; sv("#nowpage", "1");
913    scheduleCatalogScrollTop();
914    var p = payload(), value = valueMap(p, { nowpage: 1 });
915    if (range && range.reset) {
916      pendingFiltersReset = true;
917      value.reset_price = 1;
918    } else {
919      if (range && range.min !== undefined && range.min !== null && range.min !== "" && !isNaN(parseFloat(range.min))) {
920        var minVal = parseFloat(range.min);
921        value.price_min = Number(minVal.toFixed ? minVal.toFixed(2) : minVal);
922      }
923      if (range && range.max !== undefined && range.max !== null && range.max !== "" && !isNaN(parseFloat(range.max))) {
924        var maxVal = parseFloat(range.max);
925        value.price_max = Number(maxVal.toFixed ? maxVal.toFixed(2) : maxVal);
926      }
927    }
928    fpLog("price filter request", { value: value });
929    postJSON("updateFilters", { value: value }, function () {
930      refreshSessionFiltersState();
931      renderCatalogThenFilters(p);
932    });
933  }
934
935  function updateMobileFiltersResultCount() {
936    $(".mobile-filters-result-count").text(lastCatalogResultCount);
937  }
938
939  function refreshCatalogResultCount() {
940    var $catalog = $("#catalog");
941    if (!$catalog.length) return;
942    //var totalAttr = parseInt($catalog.attr("data-results-total"), 10);
943    var totalAttr = parseInt($("#data-results-total").text(), 10);
944    var count = !isNaN(totalAttr) ? totalAttr : $catalog.find(".catalog-item").length;
945    lastCatalogResultCount = count || 0;
946    updateMobileFiltersResultCount();
947  }
948
949  var shouldScrollCatalogTop = false;
950  var lastSessionFilters = null;
951  var pendingFiltersReset = false;
952  var filterAccordionUserPreference = {};
953
954  function filterAccordionKey($element) {
955    if (!$element || !$element.length) return null;
956    var $table = $element.hasClass("filter-table") ? $element : findFilterTable($element);
957    if ($table && $table.length) {
958      var tableId = $table.attr("id");
959      if (tableId) return tableId;
960    }
961    var id = $element.attr("id");
962    if (id) return id;
963    return null;
964  }
965
966  function rememberFilterAccordionPreference($element, isOpen) {
967    var key = filterAccordionKey($element);
968    if (!key) return;
969    filterAccordionUserPreference[key] = !!isOpen;
970  }
971
972  function readFilterAccordionPreference($element) {
973    var key = filterAccordionKey($element);
974    if (!key) return null;
975    return Object.prototype.hasOwnProperty.call(filterAccordionUserPreference, key)
976      ? filterAccordionUserPreference[key]
977      : null;
978  }
979
980  function scheduleCatalogScrollTop() {
981    shouldScrollCatalogTop = true;
982    // Attempt immediate scroll when the back-to-top anchor exists to improve UX on fast responses
983    var $backTop = $(".cd-top");
984    if ($backTop.length) {
985      $backTop.trigger("click");
986    } else {
987      scrollToPageTopSmooth();
988    }
989  }
990
991  function setFiltersActiveState(hasFilters) {
992    document.body.classList.toggle("catalog-filters-active", !!hasFilters);
993  }
994
995  function hasSessionFilters(filters) {
996    if (!filters || typeof filters !== "object") return false;
997    var keys = [
998      "color_filters",
999      "sizes_filters",
1000      "composition_filters",
1001      "flavor_filters",
1002      "brand_filters",      "caracteristicas_filters",
1003      "promo_filters",
1004      "stock_filters",
1005      "price_filters"
1006    ];
1007    return keys.some(function (key) {
1008      if (!Object.prototype.hasOwnProperty.call(filters, key)) return false;
1009      var value = filters[key];
1010      if (!value) return false;
1011      if (Array.isArray(value)) return value.length > 0;
1012      if (typeof value === "object") return Object.keys(value).length > 0;
1013      return !!value;
1014    });
1015  }
1016
1017  function refreshSessionFiltersState(done) {
1018    var catKey = getFilterCategoryKey();
1019    var req = postJSON("checkCurrentFilters", null, function (currentFilters) {
1020      lastSessionFilters = currentFilters || null;
1021      saveFiltersSnapshot(catKey, lastSessionFilters || {});
1022      syncFiltersStateToUrl(lastSessionFilters || {});
1023      setFiltersActiveState(hasSessionFilters(lastSessionFilters));
1024      fpLog("refreshSessionFiltersState", { catKey: catKey, snapshot: lastSessionFilters });
1025      if (typeof done === "function") done(currentFilters);
1026    });
1027    if (req && typeof req.fail === "function") {
1028      req.fail(function () {
1029        if (typeof done === "function") done(null);
1030      });
1031    }
1032    return req;
1033  }
1034
1035  function renderFilters(p, done) {
1036    var $ct = $("#menu-filters").length ? $("#menu-filters") : $("#filters");
1037    var hasExtraQueryFilters = $("#extra_query_filters").length > 0;
1038    if (!hasExtraQueryFilters) {
1039      if ($ct.length) {
1040        $ct.empty();
1041      }
1042      pendingFiltersReset = false;
1043      done && done();
1044      return;
1045    }
1046    if (!$ct.length) { pendingFiltersReset = false; done && done(); return; }
1047    postHTML("generateFilterMenu", p, function (html) {
1048      var desktopHtml = buildDesktopFiltersMarkup(html);
1049      $ct.html(desktopHtml);
1050      var $desktopScope = $ct.find(".filters-panel-content");
1051      if (!$desktopScope.length) {
1052        $desktopScope = $ct;
1053      }
1054
1055      var mobileFiltersHtml = buildMobileFiltersMarkup(html);
1056      var $mobileContainer = $("#mobile-filters-container");
1057      if ($mobileContainer.length) {
1058        $mobileContainer.html(mobileFiltersHtml);
1059        prepareMobileFiltersState($mobileContainer);
1060        initPriceFilterTables($mobileContainer);
1061      }
1062
1063      var $mobileFiltersDiv = $("#mobilefilters-div");
1064      if ($mobileFiltersDiv.length) {
1065        $mobileFiltersDiv.html(mobileFiltersHtml);
1066        prepareMobileFiltersState($mobileFiltersDiv);
1067        initPriceFilterTables($mobileFiltersDiv);
1068      }
1069
1070      openSelected($desktopScope);
1071      syncSizeFilterGrid($desktopScope);
1072      syncColorFilterGrid($desktopScope);
1073      initPriceFilterTables($desktopScope);
1074      var sessionStateKnown = lastSessionFilters && typeof lastSessionFilters === "object";
1075      var hasFiltersSession = sessionStateKnown ? hasSessionFilters(lastSessionFilters) : false;
1076      var hasFiltersDom = !isFiltersEmpty();
1077      var hasFilters = sessionStateKnown ? hasFiltersSession : hasFiltersDom;
1078      if (pendingFiltersReset) {
1079        hasFilters = false;
1080        pendingFiltersReset = false;
1081      }
1082      updateMobileFiltersBadge(hasFilters);
1083      updateMobileFiltersReset(hasFilters);
1084      updateDesktopFiltersReset(hasFilters);
1085      if (!sessionStateKnown) {
1086        setFiltersActiveState(hasFilters);
1087      }
1088      updateMobileFiltersResultCount();
1089      done && done();
1090    });
1091  }
1092
1093  function isFiltersEmpty() {
1094    if ($(".filtercolor .fa-selected, .filtersize .fa-selected, .filtercomposition .fa-selected, .filterflavor .fa-selected, .filterbrand .fa-selected, .filterfeature .fa-selected, .filterpromo .fa-selected, .filterstock .fa-selected").length) {
1095      return false;
1096    }
1097
1098    var hasInputs = false;
1099    var filters = $("#menu-filters :input, #filters :input, .filter :input");
1100    filters.each(function () {
1101      hasInputs = true;
1102      var $input = $(this);
1103      var inputType = ($input.attr("type") || "").toLowerCase();
1104      if (inputType === "hidden") {
1105        return; // hidden helpers shouldn't count as active filters
1106      }
1107      if ($(this).is(":checkbox") || $(this).is(":radio")) {
1108        if ($(this).is(":checked")) {
1109          hasInputs = false;
1110          return false;
1111        }
1112      } else {
1113        var val = $(this).val();
1114        if (val && String(val).trim() !== "") {
1115          hasInputs = false;
1116          return false;
1117        }
1118      }
1119    });
1120
1121    var $priceTable = $("#price-filter-table");
1122    if ($priceTable.length) {
1123      var bounds = getPriceFilterBounds($priceTable);
1124      var minVal = parsePriceValue($priceTable.find(".price-filter-input-min").val());
1125      var maxVal = parsePriceValue($priceTable.find(".price-filter-input-max").val());
1126      if (isFinite(minVal) && isFinite(maxVal)) {
1127        minVal = Number(minVal.toFixed(2));
1128        maxVal = Number(maxVal.toFixed(2));
1129        var minDefault = Number(bounds.min.toFixed(2));
1130        var maxDefault = Number(bounds.max.toFixed(2));
1131        if (minVal > minDefault || maxVal < maxDefault) {
1132          return false;
1133        }
1134      }
1135    }
1136
1137    return hasInputs;
1138  }
1139
1140  function updateMobileFiltersBadge(hasFilters) {
1141    var $btn = $("#mobile-filters-btn");
1142    if (!$btn.length) return;
1143    if (hasFilters) $btn.addClass("mobile-filters-btn-active");
1144    else $btn.removeClass("mobile-filters-btn-active");
1145  }
1146
1147  function renderCatalog(p, done) {
1148    var $cat = $("#catalog");
1149    if (!$cat.length) {
1150      if (typeof done === "function") done();
1151      return;
1152    }
1153    $cat.removeClass("catalog-preloader").show();
1154    postHTML("catalog", p, function (html) {
1155      $cat.html(html).removeClass("catalog-preloader").show();
1156      if ($.fn.lazy) $(".lazy").Lazy();
1157      if ($.fancybox) $('[data-fancybox]').fancybox({ loop: true });
1158      bindEvents(); // rebind após injeção
1159      refreshCatalogResultCount();
1160      if (shouldScrollCatalogTop) {
1161        shouldScrollCatalogTop = false;
1162        scrollToPageTopSmooth();
1163      }
1164      if (typeof done === "function") done();
1165    });
1166  }
1167
1168  function renderCatalogThenFilters(p, done) {
1169    var hasCatalog = $("#catalog").length > 0;
1170    var finalize = function () {
1171      renderFilters(p, done);
1172    };
1173    if (hasCatalog) {
1174      renderCatalog(p, finalize);
1175    } else {
1176      finalize();
1177    }
1178  }
1179
1180  function scrollToPageTopSmooth() {
1181    if ("scrollBehavior" in document.documentElement.style) {
1182      window.scrollTo({ top: 0, behavior: "smooth" });
1183    } else {
1184      $("html, body").stop().animate({ scrollTop: 0 }, 500);
1185    }
1186  }
1187
1188  function syncThenRender(opts) {
1189    var isReset = opts && opts.resetAll;
1190    if (isReset) pendingFiltersReset = true;
1191    var p = payload(), val = valueMap(p, isReset ? { reset_all: 1 } : null);
1192    var catKey = getFilterCategoryKey();
1193    fpLog("syncThenRender", { catKey: catKey, reset: isReset, payload: val });
1194    var renderAll = function () {
1195      renderCatalogThenFilters(p);
1196    };
1197    var req = postJSON("updateFilters", { value: val }, function () {
1198      refreshSessionFiltersState(function (filters) {
1199        saveFiltersSnapshot(catKey, filters);
1200        rememberFilterCategory(catKey);
1201        renderAll();
1202      });
1203    });
1204    if (req && typeof req.fail === "function") {
1205      req.fail(renderAll);
1206    }
1207  }
1208
1209  function toggleLiveSearch(mode) {
1210    var $search = $("#livesearch");
1211    if (!$search.length) return;
1212    var $html = $("html");
1213    var $reset = $("#livesearch-reset");
1214    var $mainBlocks = $("#main-container, #main-content, .main-footer-banners-container, .main-footer-container");
1215    var $topMenus = $("#topm-cats, #topm-info");
1216    var $header = $(".webstore-header-container");
1217    var $mobileLeft = $(".header-left-mobile-banner");
1218    var $mobileRight = $(".header-right-mobile-banner");
1219    var $toolsMobile = $("#tools-options-mobile");
1220
1221    switch (mode) {
1222      case "hide":
1223        $html.css({ overflow: "auto" }).off("touchmove.livesearch");
1224        $search.scrollTop(0).hide().empty();
1225        $("#q").val("");
1226        $reset.hide();
1227        $toolsMobile.hide();
1228        $mainBlocks.show();
1229        $topMenus.removeClass("main-container-search");
1230        $header.removeClass("webstore-header-container-open");
1231        if ($("#hamburguer").is(":visible")) {
1232          $mobileLeft.show();
1233          $mobileRight.show();
1234        }
1235        break;
1236      case "show":
1237        $mainBlocks.hide();
1238        $toolsMobile.hide();
1239        $topMenus.addClass("main-container-search");
1240        $header.addClass("webstore-header-container-open");
1241        if ($("#hamburguer").is(":visible")) {
1242          $mobileLeft.hide();
1243          $mobileRight.hide();
1244        }
1245        $reset.show().css({ display: "inline-block" });
1246        $search.show().css({ overflow: "auto" });
1247        $html.css({ overflow: "hidden" }).on("touchmove.livesearch", function (ev) {
1248          if (!$(ev.target).closest("#livesearch").length) ev.preventDefault();
1249        });
1250        break;
1251      case "reset":
1252      default:
1253        $html.css({ overflow: "auto" }).off("touchmove.livesearch");
1254        $mainBlocks.show();
1255        $toolsMobile.hide();
1256        $topMenus.removeClass("main-container-search");
1257        $header.removeClass("webstore-header-container-open");
1258        if ($("#hamburguer").is(":visible")) {
1259          $mobileLeft.show();
1260          $mobileRight.show();
1261        }
1262        $reset.hide();
1263        $search.hide().empty();
1264        break;
1265    }
1266  }
1267
1268  /* ==========================================================
1269   * 5) EVENTOS (namespaces para evitar double-binds)
1270   * ========================================================== */
1271  function goToPage(n, push) {
1272    Cat.page = String(Math.max(1, parseInt(n, 10) || 1)); sv("#nowpage", Cat.page);
1273    if (push === true) pushPage(Cat.page);
1274    syncThenRender();
1275  }
1276
1277	// Mapa de normalização de chaves para o endpoint updateFilters
1278	var KEYMAP = {
1279		filtercolor: 'color',
1280		filtersize: 'size',
1281		filtercomposition: 'composition',
1282		filterbrand: 'brand',
1283		filterflavor: 'flavor',
1284		filterfeature: 'feature',
1285		filterpromo: 'promo',
1286		filterstock: 'stock',
1287		filteritem: 'item' // se existir
1288	};
1289	function kmap(k){ return KEYMAP[k] || k; }
1290
1291  /* ==========================================================
1292   * EVENT BINDINGS (dispatcher)
1293   * ========================================================== */
1294  function bindEvents() {
1295    bindHeaderNavigation();
1296    bindFavoritesFeature();
1297    bindCurrencySwitcher();
1298    bindCatalogControls();
1299    bindFilterInteractions();
1300    bindMenuToggles();
1301    bindScrollHelpers();
1302    enableMobileStickyNav();
1303    bindProductSizeSelector();
1304    bindProductAddToCart();
1305    bindProductTabsNavigation();
1306    bindCartQuantityControls();
1307    bindDiscountHandlers();
1308    bindCheckoutFlowEvents();
1309    bindDeliveryApiLanguageSwitch();
1310    bindGiftAndInfoToggles();
1311    bindLiveSearchEvents();
1312    bindCartRemovalEvents();
1313    initOrderForm();
1314  }
1315
1316  function bindHeaderNavigation() {
1317    $(document)
1318      .off("click.cartNav")
1319      .on("click.cartNav", ".shopcart-container > .shopcart-selector, .shopcart-container .shopcart-badge, .shopcart-selector-small", function (e) {
1320        e.stopPropagation(); // um so open por clique (badge e selector partilham o container)
1321        e.preventDefault();
1322        if (typeof window.openShopcartDrawer === "function") {
1323          window.openShopcartDrawer();
1324          return;
1325        }
1326        var lang = Cookies.get("clang_id") || "pt";
1327        window.location.assign("/" + lang + "/cart/");
1328      });
1329
1330    $(document)
1331      .off("click.cartNavModal")
1332      .on("click.cartNavModal", ".fichaGotoCartModal, .fichaGotoCart", function (e) {
1333        e.preventDefault();
1334        if (typeof window.openShopcartDrawer === "function") {
1335          window.openShopcartDrawer();
1336          return;
1337        }
1338        var lang = Cookies.get("clang_id") || "pt";
1339        window.location.assign("/" + lang + "/checkout/");
1340      });
1341
1342    $(document)
1343      .off("click.logout")
1344      .on("click.logout", ".logout", function (e) {
1345        e.preventDefault();
1346        logoutUser();
1347      });
1348  }
1349
1350  function bindFavoritesFeature() {
1351    var favoritesListsEnabled = !!(hasWindow && window.CUPIDO_FAVORITES_LISTS_ENABLED);
1352
1353    function favoritesModalMarkup() {
1354      return [
1355        "<div class='favorites-lists-modal' id='favoritesListsModal'>",
1356        "  <div class='favorites-lists-modal__dialog'>",
1357        "    <button type='button' class='favorites-lists-modal__close' aria-label='Fechar'><i class='fa fa-times'></i></button>",
1358        "    <div class='favorites-lists-modal__view is-active' data-view='lists'>",
1359        "      <h3 class='favorites-lists-modal__title'>Gerir favoritos</h3>",
1360        "      <button type='button' class='favorites-lists-modal__create-link'>Criar nova lista</button>",
1361        "      <div class='favorites-lists-modal__lists'></div>",
1362        "    </div>",
1363        "    <div class='favorites-lists-modal__view' data-view='create'>",
1364        "      <div class='favorites-lists-modal__topline'><button type='button' class='favorites-lists-modal__back'><i class='fa fa-arrow-left'></i></button><h3 class='favorites-lists-modal__title'>Criar nova lista</h3></div>",
1365        "      <input type='text' class='favorites-lists-modal__input' placeholder='Nome da lista'>",
1366        "      <button type='button' class='favorites-lists-modal__submit'>Criar lista</button>",
1367        "    </div>",
1368        "  </div>",
1369        "</div>"
1370      ].join("");
1371    }
1372
1373    function ensureFavoritesModal() {
1374      if (!favoritesListsEnabled) return $();
1375      var $modal = $("#favoritesListsModal");
1376      if ($modal.length) return $modal;
1377      $("body").append(favoritesModalMarkup());
1378      if (!$("#favoritesListsModalStyles").length) {
1379        $("head").append([
1380          "<style id='favoritesListsModalStyles'>",
1381          ".favorites-lists-modal{position:fixed;inset:0;z-index:99999;background:rgba(15,23,42,.45);display:none;align-items:center;justify-content:center;padding:20px}",
1382          ".favorites-lists-modal.is-open{display:flex}",
1383          ".favorites-lists-modal__dialog{width:min(100%,560px);max-height:min(90vh,720px);overflow:auto;background:#fff;border-radius:24px;padding:22px 22px 18px;box-shadow:0 30px 80px rgba(15,23,42,.28);position:relative}",
1384          ".favorites-lists-modal__close{position:absolute;top:16px;right:16px;width:40px;height:40px;border:0;background:#fff;color:#94a3b8;font-size:22px;cursor:pointer}",
1385          ".favorites-lists-modal__title{margin:0 0 18px;font:700 24px 'Raleway',sans-serif;color:#1f2937}",
1386          ".favorites-lists-modal__create-link{padding:0;border:0;background:none;color:#3b82f6;font:500 18px 'Raleway',sans-serif;cursor:pointer;margin-bottom:12px}",
1387          ".favorites-lists-modal__lists{display:flex;flex-direction:column;border-top:1px solid #e5e7eb}",
1388          ".favorites-lists-modal__item{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 4px;border-bottom:1px solid #e5e7eb;cursor:pointer}",
1389          ".favorites-lists-modal__item.is-selected{background:#eff6ff}",
1390          ".favorites-lists-modal__item-left{display:flex;align-items:center;gap:14px;min-width:0}",
1391          ".favorites-lists-modal__icon{font-size:32px;color:#111827;min-width:34px;text-align:center}",
1392          ".favorites-lists-modal__name{font:600 18px 'Raleway',sans-serif;color:#1f2937}",
1393          ".favorites-lists-modal__check{font-size:28px;color:#111827;min-width:28px;text-align:center}",
1394          ".favorites-lists-modal__view{display:none}",
1395          ".favorites-lists-modal__view.is-active{display:block}",
1396          ".favorites-lists-modal__topline{display:flex;align-items:center;gap:14px;margin-bottom:18px}",
1397          ".favorites-lists-modal__back{padding:0;border:0;background:none;font-size:28px;color:#111827;cursor:pointer}",
1398          ".favorites-lists-modal__input{width:100%;height:58px;padding:0 16px;border:2px solid #3b82f6;border-radius:14px;font:500 18px 'Raleway',sans-serif;color:#1f2937;box-sizing:border-box}",
1399          ".favorites-lists-modal__submit{margin-top:16px;width:100%;height:58px;border:0;border-radius:14px;background:#2975f0;color:#fff;font:700 20px 'Raleway',sans-serif;cursor:pointer}",
1400          "@media (max-width:640px){.favorites-lists-modal{padding:12px}.favorites-lists-modal__dialog{border-radius:20px;padding:18px 18px 16px}.favorites-lists-modal__title{font-size:21px}.favorites-lists-modal__name{font-size:17px}}",
1401          "</style>"
1402        ].join(""));
1403      }
1404      return $("#favoritesListsModal");
1405    }
1406
1407    function setFavoriteVisual($btn, hasFavorites) {
1408      var isCatalog = $btn.hasClass("catalog_favorite_add") || $btn.hasClass("catalog_favorite_remove");
1409      var isLive = $btn.hasClass("livesearch_favorite_add") || $btn.hasClass("livesearch_favorite_remove");
1410      if (isCatalog) {
1411        if (hasFavorites) {
1412          $btn.removeClass("catalog_favorite_add far fa-heart-o").addClass("catalog_favorite_remove fa fa-heart");
1413        } else {
1414          $btn.removeClass("catalog_favorite_remove fa fa-heart").addClass("catalog_favorite_add far fa-heart-o");
1415        }
1416        return;
1417      }
1418      if (isLive) {
1419        if (hasFavorites) {
1420          $btn.removeClass("livesearch_favorite_add fa-heart-o far").addClass("livesearch_favorite_remove fa fa-heart");
1421        } else {
1422          $btn.removeClass("livesearch_favorite_remove fa fa-heart").addClass("livesearch_favorite_add far fa-heart-o");
1423        }
1424        return;
1425      }
1426      if ($btn.is("input")) {
1427        if (hasFavorites) {
1428          $btn.removeClass("shopcart_favorite_add catalog_favorite_add").addClass("shopcart_favorite_remove btnFavoritesSelected").val(gl("BTN_FICHA_REMOVER_FAVORITOS") || "Remover favorito");
1429        } else {
1430          $btn.removeClass("shopcart_favorite_remove catalog_favorite_remove btnFavoritesSelected").addClass("shopcart_favorite_add").val(gl("BTN_FICHA_ADD_FAVORITOS") || "Adicionar favorito");
1431        }
1432        return;
1433      }
1434      if (hasFavorites) {
1435        $btn.removeClass("shopcart_favorite_add catalog_favorite_add").addClass("shopcart_favorite_remove btnFavoritesSelected").html("<i class='fas fa-heart fa-2x'></i><br>" + (gl("LIVES_REM_FAVORITE") || "Remover favorito"));
1436      } else {
1437        $btn.removeClass("shopcart_favorite_remove catalog_favorite_remove btnFavoritesSelected").addClass("shopcart_favorite_add").html("<i class='far fa-heart fa-2x'></i><br>" + (gl("LIVES_ADD_FAVORITE") || "Adicionar favorito"));
1438      }
1439    }
1440
1441    function getFavoriteContext($btn) {
1442      var productId =
1443        $btn.data("id_code") ||
1444        $btn.attr("id_code") ||
1445        $btn.data("id") ||
1446        $btn.attr("data-id") ||
1447        $btn.attr("rel") ||
1448        $("#id_code").val() ||
1449        "";
1450      var elementId = 0;
1451      var catalogElements = parseInt($btn.attr("elementos") || $btn.data("elementos") || "0", 10) || 0;
1452      var hasSizeSelector = $("#tabela_tamanhos .sizebox").length > 0;
1453      var $activeSize = $("#tabela_tamanhos .sizebox.size_btn_active:first");
1454      if ($activeSize.length) {
1455        elementId = parseInt($activeSize.attr("rel") || $activeSize.data("id") || $activeSize.data("rel") || "0", 10) || 0;
1456      }
1457      return {
1458        productId: productId,
1459        elementId: elementId,
1460        requiresVariant: hasSizeSelector || catalogElements > 0,
1461        hasVariantSelected: elementId > 0
1462      };
1463    }
1464
1465    function syncFavoriteButtons(productId, hasFavorites) {
1466      var selector = "[id_code='" + productId + "'],[data-id_code='" + productId + "'],[data-id='" + productId + "'],[rel='" + productId + "']";
1467      $(selector).filter(".shopcart_favorite_add, .shopcart_favorite_remove, #shopcart_favorite_add, #shopcart_favorite_remove, .catalog_favorite_add, .catalog_favorite_remove, .btnFavorites, .livesearch_favorite_add, .livesearch_favorite_remove").each(function () {
1468        setFavoriteVisual($(this), hasFavorites);
1469      });
1470    }
1471
1472    function refreshFavoritesFragments() {
1473      if (typeof window.favoritesMenu === "function") window.favoritesMenu();
1474      if (typeof window.favoritesSHreminder === "function") window.favoritesSHreminder();
1475      if (typeof window.loadFavoritesListsPanel === "function") window.loadFavoritesListsPanel();
1476    }
1477
1478    function renderFavoritesLists($modal, response) {
1479      var selected = response && response.selected_ids ? response.selected_ids : [];
1480      var lists = response && response.lists ? response.lists : [];
1481      var html = "";
1482      if (!lists.length) {
1483        html = "<div class='favorites-lists-modal__item'><div class='favorites-lists-modal__name'>Ainda não tem listas criadas.</div></div>";
1484      } else {
1485        $.each(lists, function (_, item) {
1486          var isSelected = $.inArray(String(item.id_lista), $.map(selected, String)) !== -1;
1487          html += "<div class='favorites-lists-modal__item" + (isSelected ? " is-selected" : "") + "' data-list-id='" + item.id_lista + "'>" +
1488            "<div class='favorites-lists-modal__item-left'><i class='fa fa-bookmark favorites-lists-modal__icon'></i><span class='favorites-lists-modal__name'>" + $("<div>").text(item.nome_lista).html() + "</span></div>" +
1489            "<div class='favorites-lists-modal__check'>" + (isSelected ? "<i class='fa fa-check'></i>" : "") + "</div></div>";
1490        });
1491      }
1492      $modal.find(".favorites-lists-modal__lists").html(html);
1493    }
1494
1495    function openFavoritesListsModal(productId, elementId) {
1496      var $modal = ensureFavoritesModal();
1497      if (!$modal.length) return;
1498      $modal.data("product-id", productId).data("element-id", elementId || 0);
1499      $modal.find(".favorites-lists-modal__view").removeClass("is-active");
1500      $modal.find("[data-view='lists']").addClass("is-active");
1501      $modal.find(".favorites-lists-modal__lists").html("<div class='favorites-lists-modal__item'><div class='favorites-lists-modal__name'>A carregar...</div></div>");
1502      $.post(REQUEST_ENDPOINT, { ajax: "favorites_lists_modal", id_produto: productId, id_produto_elemento: elementId || 0 }, function (response) {
1503        if (response && response.message === "login_required") {
1504          closeFavoritesListsModal();
1505          var lang = ($("#session_lang").val() || "pt");
1506          fetchLangDict().always(function () {
1507            swal({
1508              title: gl("CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TIT
1508LE") || "Listas e Favoritos",
1509              text: gl("CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_TEXT") || "Para usar a funcionalidade de Listas e Favoritos, por favor faça login ou registe-se",
1510              type: "info",
1511              showCancelButton: true,
1512              confirmButtonColor: "#abd559",
1513              cancelButtonColor: "#d9dee7",
1514              confirmButtonText: gl("CUSTOMER_AREA_FAVORITES_LOGIN_REQUIRED_REGISTER") || "Registar",
1515              cancelButtonText: gl("swal_btnok") || "OK",
1516              closeOnConfirm: true,
1517              closeOnCancel: true,
1518              animation: false
1519            }, function (isConfirm) {
1520              if (isConfirm) {
1521                window.location.href = "/" + lang + "/account/";
1522              }
1523            });
1524          });
1525          return;
1526        }
1527        if (!response || !response.ok) {
1528          toastr.error("Não foi possível carregar as listas.");
1529          return;
1530        }
1531        $modal.addClass("is-open");
1532        renderFavoritesLists($modal, response);
1533        $modal.data("has-favorites", !!response.has_favorites);
1534        syncFavoriteButtons(productId, !!response.has_favorites);
1535      }, "json").fail(function () {
1536        closeFavoritesListsModal();
1537        toastr.error("Não foi possível carregar as listas.");
1538      });
1539    }
1540
1541    function closeFavoritesListsModal() {
1542      $("#favoritesListsModal").removeClass("is-open");
1543    }
1544
1545    if (favoritesListsEnabled) {
1546      $(document)
1547        .off("click.favoritesAll")
1548        .on("click.favoritesAll",
1549          ".shopcart_favorite_add, .shopcart_favorite_remove, #shopcart_favorite_add, #shopcart_favorite_remove, .catalog_favorite_add, .catalog_favorite_remove, .btnFavorites, .livesearch_favorite_add, .livesearch_favorite_remove",
1550          function (e) {
1551            e.preventDefault();
1552            e.stopImmediatePropagation();
1553            var $btn = $(this);
1554            var ctx = getFavoriteContext($btn);
1555            if (!ctx.productId) {
1556              toastr.error("Não consegui identificar o produto.");
1557              return;
1558            }
1559            if (ctx.requiresVariant && !ctx.hasVariantSelected) {
1560              toastr.warning("Selecione a opção pretendida antes de guardar nos favoritos.");
1561              return;
1562            }
1563            openFavoritesListsModal(ctx.productId, ctx.elementId);
1564          });
1565
1566      $(document)
1567        .off("click.favmenuRemove")
1568        .on("click.favmenuRemove", ".favmenu-remove", function (e) {
1569          e.preventDefault();
1570          var id_code = $(this).data("id_code") || $(this).attr("id_code") || $(this).attr("rel") || "";
1571          if (!id_code) return;
1572          $.post(REQUEST_ENDPOINT, { ajax: "favorites_lists_remove_all_product", id_produto: id_code, id_produto_elemento: 0 }, function () {
1573            syncFavoriteButtons(id_code, false);
1574            refreshFavoritesFragments();
1575            toastr.info(gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos");
1576          }, "json");
1577        });
1578
1579      $(document)
1580        .off("click.favoritesModalClose")
1581        .on("click.favoritesModalClose", ".favorites-lists-modal__close, .favorites-lists-modal__back", function () {
1582          var $modal = $("#favoritesListsModal");
1583          if ($(this).hasClass("favorites-lists-modal__back")) {
1584            $modal.find(".favorites-lists-modal__view").removeClass("is-active");
1585            $modal.find("[data-view='lists']").addClass("is-active");
1586            return;
1587          }
1588          closeFavoritesListsModal();
1589        })
1590        .off("click.favoritesModalOverlay")
1591        .on("click.favoritesModalOverlay", "#favoritesListsModal", function (e) {
1592          if (e.target === this) closeFavoritesListsModal();
1593        })
1594        .off("click.favoritesModalCreateLink")
1595        .on("click.favoritesModalCreateLink", ".favorites-lists-modal__create-link", function () {
1596          var $modal = $("#favoritesListsModal");
1597          $modal.find(".favorites-lists-modal__view").removeClass("is-active");
1598          $modal.find("[data-view='create']").addClass("is-active");
1599          $modal.find(".favorites-lists-modal__input").val("").focus();
1600        })
1601        .off("click.favoritesModalSubmit")
1602        .on("click.favoritesModalSubmit", ".favorites-lists-modal__submit", function () {
1603          var $modal = $("#favoritesListsModal");
1604          var productId = $modal.data("product-id");
1605          var elementId = $modal.data("element-id") || 0;
1606          var name = $.trim($modal.find(".favorites-lists-modal__input").val());
1607          if (!name) return;
1608          $.post(REQUEST_ENDPOINT, { ajax: "favorites_lists_create", nome_lista: name, id_produto: productId, id_produto_elemento: elementId }, function (response) {
1609            if (!response || !response.ok) {
1610              toastr.error("Não foi possível criar a lista.");
1611              return;
1612            }
1613            $modal.find(".favorites-lists-modal__view").removeClass("is-active");
1614            $modal.find("[data-view='lists']").addClass("is-active");
1615            renderFavoritesLists($modal, response);
1616            syncFavoriteButtons(productId, !!response.has_favorites);
1617            refreshFavoritesFragments();
1618          }, "json");
1619        })
1620        .off("click.favoritesModalItem")
1621        .on("click.favoritesModalItem", ".favorites-lists-modal__item[data-list-id]", function () {
1622          var $item = $(this);
1623          var $modal = $("#favoritesListsModal");
1624          var productId = $modal.data("product-id");
1625          var elementId = $modal.data("element-id") || 0;
1626          var listId = $item.data("list-id");
1627          var selected = !$item.hasClass("is-selected");
1628          $.post(REQUEST_ENDPOINT, { ajax: "favorites_lists_toggle_product", id_lista: listId, id_produto: productId, id_produto_elemento: elementId, selected: selected ? 1 : 0 }, function (response) {
1629            if (!response || !response.ok) {
1630              toastr.error("Não foi possível atualizar os favoritos.");
1631              return;
1632            }
1633            renderFavoritesLists($modal, response);
1634            syncFavoriteButtons(productId, !!response.has_favorites);
1635            refreshFavoritesFragments();
1636            toastr[selected ? "success" : "info"](selected ? (gl("LIVES_ADD_FAVORITE_INFO") || "Adicionado aos favoritos") : (gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos"));
1637          }, "json");
1638        });
1639
1640      window.favoritesMenu = function () {
1641        $.post(REQUEST_ENDPOINT, { ajax: "favoritesMenu" }, function (html) {
1642          $(".favorites-menu, #favorites-menu, .divfavorites-menu").html(html);
1643          if ($.fn.lazy) $(".lazy").Lazy();
1644        });
1645      };
1646      return;
1647    }
1648
1649    $(document)
1650      .off("click.favoritesAll")
1651      .on("click.favoritesAll",
1652        ".shopcart_favorite_add, .shopcart_favorite_remove, " +
1653        ".catalog_favorite_add, .catalog_favorite_remove, " +
1654        ".btnFavorites",
1655        function (e) {
1656          e.preventDefault();
1657
1658          var $btn = $(this);
1659          var id_code =
1660            $btn.data("id_code") ||
1661            $btn.attr("id_code") ||
1662            $btn.data("id") ||
1663            $btn.attr("data-id") ||
1664            $btn.attr("rel") ||
1665            $("#id_code").val() ||
1666            "";
1667
1668          if (!id_code) {
1669            toastr.error("Não consegui identificar o produto.");
1670            return;
1671          }
1672
1673          var isRemove =
1674            $btn.hasClass("shopcart_favorite_remove") ||
1675            $btn.hasClass("catalog_favorite_remove") ||
1676            $btn.hasClass("btnFavoritesSelected");
1677
1678          var action = isRemove ? "shopcart_favorites_remove" : "shopcart_favorites_add";
1679          var isCatalog = $btn.hasClass("catalog_favorite_add") || $btn.hasClass("catalog_favorite_remove");
1680
1681          $.ajax({
1682            type: "POST",
1683            url: REQUEST_ENDPOINT,
1684            data: { ajax: action, id_code: id_code },
1685            success: function () {
1686              if (isCatalog) {
1687                if (action === "shopcart_favorites_add") {
1688                  toastr.info(gl("LIVES_ADD_FAVORITE_INFO") || "Adicionado aos favoritos");
1689                  $btn.removeClass("catalog_favorite_add far fa-heart").addClass("catalog_favorite_remove fa fa-heart");
1690                } else {
1691                  toastr.warning(gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos");
1692                  $btn.removeClass("catalog_favorite_remove fa fa-heart").addClass("catalog_favorite_add far fa-heart");
1693                }
1694              } else {
1695                if (action === "shopcart_favorites_add") {
1696                  toastr.success(gl("LIVES_ADD_FAVORITE_INFO") || "Adicionado aos favoritos");
1697                  $btn
1698                    .removeClass("shopcart_favorite_add catalog_favorite_add")
1699                    .addClass("shopcart_favorite_remove btnFavoritesSelected")
1700                    .html("<i class='fas fa-heart fa-2x'></i><br>" + (gl("LIVES_REM_FAVORITE") || "Remover favorito"));
1701                } else {
1702                  toastr.info(gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos");
1703                  $btn
1704                    .removeClass("shopcart_favorite_remove catalog_favorite_remove btnFavoritesSelected")
1705                    .addClass("shopcart_favorite_add")
1706                    .html("<i class='far fa-heart fa-2x'></i><br>" + (gl("LIVES_ADD_FAVORITE") || "Adicionar favorito"));
1707                }
1708              }
1709
1710              if (typeof window.favoritesMenu === "function") {
1711                window.favoritesMenu();
1712              } else {
1713                $.post(REQUEST_ENDPOINT, { ajax: "favoritesMenu" }, function (html) {
1714                  $(".favorites-menu, #favorites-menu, .divfavorites-menu").html(html);
1715                  if ($.fn.lazy) $(".lazy").Lazy();
1716                });
1717              }
1718            }
1719          });
1720        });
1721
1722    $(document)
1723      .off("click.favmenuRemove")
1724      .on("click.favmenuRemove", ".favmenu-remove", function (e) {
1725        e.preventDefault();
1726        var id_code = $(this).data("id_code") || $(this).attr("id_code") || $(this).attr("rel") || "";
1727        if (!id_code) return;
1728
1729        $.post(REQUEST_ENDPOINT, { ajax: "shopcart_favorites_remove", id_code: id_code }, function () {
1730          toastr.info(gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos");
1731
1732          if (typeof window.favoritesMenu === "function") {
1733            window.favoritesMenu();
1734          }
1735
1736          if (typeof window.favoritesSHreminder === "function") {
1737            window.favoritesSHreminder();
1738          }
1739        });
1740      });
1741
1742    window.favoritesMenu = function () {
1743      $.post(REQUEST_ENDPOINT, { ajax: "favoritesMenu" }, function (html) {
1744        $(".favorites-menu, #favorites-menu, .divfavorites-menu").html(html);
1745        if ($.fn.lazy) $(".lazy").Lazy();
1746      });
1747    };
1748  }
1749
1750  function bindCurrencySwitcher() {
1751    $(document)
1752      .off("click.cur")
1753      .on("click.cur", ".tools-price", function (e) {
1754        e.preventDefault();
1755
1756        var raw = (this.dataset.cur || $(this).attr("href") || $(this).text() || "").toLowerCase().trim();
1757        var MAP = {
1758          'usd': 'usd', 'dolar': 'usd', 'dólar': 'usd',
1759          'gbp': 'libras', 'libras': 'libras', 'libra': 'libras',
1760          'eur': 'euros', 'euro': 'euros', 'euros': 'euros',
1761          'brl': 'reais', 'real': 'reais', 'reais': 'reais'
1762        };
1763
1764        var cur = MAP[raw] || 'euros';
1765    $('#tools-options').fadeOut(80);
1766    window.location.assign('/?cur=' + cur);
1767  });
1768  }
1769    $(document)
1770      .off("click.mobileTools")
1771      .on("click.mobileTools", "#mobile-tools-selector", function (e) {
1772        e.preventDefault();
1773        $("#tools-options-mobile").toggle();
1774      });
1775
1776  function bindCatalogControls() {
1777    $(document).off(".cat");
1778    if (!Cat.order) {
1779      Cat.order = $(".webstore-order:first").val() || "";
1780    }
1781
1782    $(document).on("change.cat", ".webstore-order", function () {
1783      Cat.order = $(this).val() || "1";
1784      scheduleCatalogScrollTop();
1785      goToPage(1, false);
1786    });
1787
1788    $(document).on("change.cat", ".webstore-results-num", function () {
1789      Cat.perPage = $(this).val() || "32";
1790      scheduleCatalogScrollTop();
1791      goToPage(1, false);
1792    });
1793
1794    $(document).on("change.cat", ".webstore-direct-page, .webstore-page-num", function () {
1795      goToPage($(this).val(), true);
1796    });
1797
1798    $(document).on("click.cat", ".webstore-nav, #next-catalog-page, #prev-catalog-page, [data-page]", function (e) {
1799      e.preventDefault();
1800      var targetPage = $(this).attr("page") || $(this).data("page");
1801      if (targetPage) {
1802        scheduleCatalogScrollTop();
1803        goToPage(targetPage, true);
1804      }
1805    });
1806  }
1807
1808  function findFilterTable($header) {
1809    if (!$header || !$header.length) return $();
1810    var $table = $header.next(".filter-table");
1811    if (!$table.length) {
1812      $table = $header.nextAll(".filter-table").first();
1813    }
1814    return $table;
1815  }
1816
1817  function findFilterHeader($table) {
1818    if (!$table || !$table.length) return $();
1819    return $table.prevAll("[id$='-filter-header'], .sub_filters_container").first();
1820  }
1821
1822  function setFilterAccordionState($target, shouldOpen, options) {
1823    if (!$target || !$target.length) return;
1824    var $header, $table;
1825    if ($target.hasClass("filter-table")) {
1826      $table = $target;
1827      $header = findFilterHeader($table);
1828    } else {
1829      $header = $target;
1830      $table = findFilterTable($header);
1831    }
1832    if (!$table || !$table.length) return;
1833    if ($header && $header.length) {
1834      $header
1835        .toggleClass("is-open", shouldOpen)
1836        .attr("aria-expanded", shouldOpen ? "true" : "false");
1837    }
1838    $table.toggleClass("is-open", shouldOpen).css("display", "");
1839    if (options && options.persist) {
1840      rememberFilterAccordionPreference($header && $header.length ? $header : $table, shouldOpen);
1841    }
1842  }
1843
1844  function toggleFilterAccordion($header) {
1845    if (!$header || !$header.length) return;
1846    var $table = findFilterTable($header);
1847    if (!$table.length) return;
1848    var willOpen = !$table.hasClass("is-open");
1849    setFilterAccordionState($header, willOpen, { persist: true });
1850  }
1851
1852  function bindFilterInteractions() {
1853    $(document)
1854      .off("click.catFilters")
1855      .on("click.catFilters",
1856        "#color-filter-header, #size-filter-header, #composition-filter-header, #brand-filter-header, #feature-filter-header, #flavor-filter-header, #price-filter-header",
1857        function (e) {
1858          e.preventDefault();
1859          toggleFilterAccordion($(this));
1860        });
1861
1862    $(document)
1863      .off("click.catFiltersResetDesktop")
1864      .on("click.catFiltersResetDesktop", ".filters-panel-reset", function (e) {
1865        if ($(this).prop("disabled")) return;
1866        e.preventDefault();
1867        pendingFiltersReset = true;
1868        updateDesktopFiltersReset(false);
1869        updateMobileFiltersReset(false);
1870        Cat.page = "1"; sv("#nowpage", "1");
1871        scheduleCatalogScrollTop();
1872        syncThenRender({ resetAll: true });
1873      });
1874
1875    $(document)
1876      .off("click.catFilterToggle")
1877      .on("click.catFilterToggle", ".filtercolor, .filtersize, .filtercomposition, .filterflavor, .filterbrand, .filterfeature, .filterpromo, .filterstock", function (e) {
1878      e.preventDefault();
1879      var $a = $(this);
1880      var keyRaw = $a.data("key") ||
1881        ($a.hasClass("filtercolor") ? "filtercolor" :
1882        $a.hasClass("filtersize") ? "filtersize" :
1883        $a.hasClass("filtercomposition") ? "filtercomposition" :
1884        $a.hasClass("filterbrand") ? "filterbrand" :
1885        $a.hasClass("filterflavor") ? "filterflavor" :
1886        $a.hasClass("filterfeature") ? "filterfeature" :
1887        $a.hasClass("filterpromo") ? "filterpromo" :
1888        $a.hasClass("filterstock") ? "filterstock" : null);
1889
1890      var key = kmap(keyRaw);
1891      var val = $a.data("value") || $a.attr("value") || $a.attr("id") || $.trim($a.text());
1892
1893      Cat.page = "1"; sv("#nowpage", "1");
1894      scheduleCatalogScrollTop();
1895      var p = payload(), value = valueMap(p, { nowpage: 1 });
1896      value[key] = val;
1897
1898      $.ajax({
1899        type: "POST",
1900        url: REQUEST_ENDPOINT,
1901        data: { ajax: "updateFilters", value: value },
1902        success: function () {
1903          refreshSessionFiltersState();
1904          renderCatalogThenFilters(p);
1905        }
1906      });
1907    });
1908
1909    $(document).on("change.cat", "#menu-filters :input
1909, #filters :input, .filter :input", function () {
1910      Cat.page = "1"; sv("#nowpage", "1");
1911      scheduleCatalogScrollTop();
1912      var p = payload(), value = valueMap(p, { nowpage: 1 });
1913      $("#menu-filters :input, #filters :input, .filter :input").each(function () {
1914        if (!this.name) return;
1915        var name = kmap(this.name.replace(/^value\[|\]$/g, ""));
1916        value[name] = $(this).is(":checkbox")
1917          ? ($(this).is(":checked") ? ($(this).val() || 1) : 0)
1918          : $(this).val();
1919      });
1920      postJSON("updateFilters", { value: value }, function () {
1921        refreshSessionFiltersState();
1922        renderCatalogThenFilters(p);
1923      });
1924    });
1925
1926    $(document)
1927      .off("change.priceFilterRange")
1928      .on("change.priceFilterRange", ".price-filter-range", function () {
1929        var $table = $(this).closest(".price-filter-table");
1930        if (!$table.length) return;
1931        var bounds = getPriceFilterBounds($table);
1932        var minVal = parsePriceValue($table.find(".price-filter-range-min").val());
1933        var maxVal = parsePriceValue($table.find(".price-filter-range-max").val());
1934        var range = normalizePriceRange(minVal, maxVal, bounds, $(this).hasClass("price-filter-range-min") ? "min" : "max");
1935        syncPriceFilterFields($table, range);
1936        fpLog("price slider change", range);
1937        sendPriceFilterRequest(range);
1938      });
1939
1940    $(document)
1941      .off("change.priceFilterInput")
1942      .on("change.priceFilterInput", ".price-filter-input", function () {
1943        var $table = $(this).closest(".price-filter-table");
1944        if (!$table.length) return;
1945        var bounds = getPriceFilterBounds($table);
1946        var minVal = parsePriceValue($table.find(".price-filter-input-min").val());
1947        var maxVal = parsePriceValue($table.find(".price-filter-input-max").val());
1948        var range = normalizePriceRange(minVal, maxVal, bounds, $(this).hasClass("price-filter-input-min") ? "min" : "max");
1949        syncPriceFilterFields($table, range);
1950        fpLog("price input change", range);
1951        sendPriceFilterRequest(range);
1952      });
1953
1954    $(document)
1955      .off("click.priceFilterApply")
1956      .on("click.priceFilterApply", ".price-filter-apply", function (e) {
1957        e.preventDefault();
1958        var $table = $(this).closest(".price-filter-table");
1959        if (!$table.length) return;
1960        var bounds = getPriceFilterBounds($table);
1961        var minVal = parsePriceValue($table.find(".price-filter-input-min").val());
1962        var maxVal = parsePriceValue($table.find(".price-filter-input-max").val());
1963        if (!isFinite(minVal) && !isFinite(maxVal)) {
1964          sendPriceFilterRequest({ reset: true });
1965        } else {
1966          var range = normalizePriceRange(minVal, maxVal, bounds);
1967          sendPriceFilterRequest(range);
1968        }
1969      });
1970
1971    $(document)
1972      .off("click.priceFilterReset")
1973      .on("click.priceFilterReset", ".price-filter-reset", function (e) {
1974        e.preventDefault();
1975        var $table = $(this).closest(".price-filter-table");
1976        if ($table.length) {
1977          var bounds = getPriceFilterBounds($table);
1978          syncPriceFilterFields($table, bounds);
1979        }
1980        sendPriceFilterRequest({ reset: true });
1981      });
1982  }
1983
1984  function setMobileFiltersState(shouldOpen) {
1985    var $container = $("#mobile-filters-container");
1986    if (!$container.length) return;
1987    if (typeof shouldOpen === "undefined") {
1988      shouldOpen = $container.hasClass("mobile-filters-hide");
1989    }
1990    $container.toggleClass("mobile-filters-hide", !shouldOpen);
1991    $("body").css("overflow", shouldOpen ? "hidden" : "auto");
1992  }
1993
1994  function bindMenuToggles() {
1995    $(document)
1996      .off("click.mobileMainMenu")
1997      .on("click.mobileMainMenu", ".mobile-mainmenu", function (e) {
1998        e.preventDefault();
1999        var id = $(this).attr("id_code");
2000        if (!id) return;
2001        $("#subcat_" + id).toggleClass("mobile-submenu-open");
2002        $(this).find(".menu-sub-icon").toggleClass("fa-plus fa-minus");
2003      });
2004
2005    $(document)
2006      .off("click.desktopMainMenu")
2007      .on("click.desktopMainMenu", ".desktop-mainmenu", function (e) {
2008        e.preventDefault();
2009        var id = $(this).attr("id_code");
2010        if (!id) return;
2011        $("#desktop_subcat_" + id).toggleClass("desktop-submenu-open");
2012        $(this).find(".menu-sub-icon").toggleClass("fa-plus fa-minus");
2013      });
2014
2015    $(".mobile_maincat.active").each(function () {
2016      var id = $(this).attr("id_code");
2017      if (id) {
2018        $("#plus_" + id).trigger("click");
2019      }
2020    });
2021
2022    $(".desktop_maincat.active").each(function () {
2023      var id = $(this).attr("id_code");
2024      if (id) {
2025        $("#desktop_plus_" + id).trigger("click");
2026      }
2027    });
2028
2029    $(document)
2030      .off("click.hamburguer")
2031      .on("click.hamburguer", "#hamburguer", function (e) {
2032        e.preventDefault();
2033        toggleLeftMenu();
2034      });
2035
2036    $(document)
2037      .off("click.mobileFiltersToggle")
2038      .on("click.mobileFiltersToggle", ".mobile-filters-close, #mobile-filters-btn", function (e) {
2039        e.preventDefault();
2040        if (typeof updatefilters === "function") updatefilters();
2041        var $container = $("#mobile-filters-container");
2042        var openNext = $container.hasClass("mobile-filters-hide");
2043        setMobileFiltersState(openNext);
2044      });
2045
2046    $(document)
2047      .off("click.mobileFiltersReset")
2048      .on("click.mobileFiltersReset", ".mobile-filters-reset", function (e) {
2049        if ($(this).is(":disabled")) return;
2050        e.preventDefault();
2051        pendingFiltersReset = true;
2052        updateMobileFiltersReset(false);
2053        updateDesktopFiltersReset(false);
2054        Cat.page = "1"; sv("#nowpage", "1");
2055        scheduleCatalogScrollTop();
2056        syncThenRender({ resetAll: true });
2057      });
2058
2059    $(document)
2060      .off("click.mobileFiltersApply")
2061      .on("click.mobileFiltersApply", ".mobile-filters-apply", function (e) {
2062        e.preventDefault();
2063        setMobileFiltersState(false);
2064      });
2065
2066  }
2067
2068  function bindScrollHelpers() {
2069    var $toTop = $(".cd-top");
2070    $(window).off("scroll.ux").on("scroll.ux", function () {
2071      if ($(this).scrollTop() > 300) $toTop.addClass("cd-is-visible"); else $toTop.removeClass("cd-is-visible cd-fade-out");
2072      if ($(this).scrollTop() > 1200) $toTop.addClass("cd-fade-out");
2073      if ($.fn.lazy) $(".lazy").lazy();
2074    });
2075
2076    $toTop.off("click.ux").on("click.ux", function (e) {
2077      e.preventDefault();
2078      $("body,html").animate({ scrollTop: 0 }, 700);
2079    });
2080  }
2081
2082  function bindProductSizeSelector() {
2083    $(document)
2084      .off("click.prodSize")
2085      .on("click.prodSize", ".sizebox", function (e) {
2086        e.preventDefault();
2087
2088        var $btn      = $(this);
2089        var id_code   = $btn.attr("rel") || $btn.data("id") || $btn.data("rel");
2090        var stock     = parseInt($btn.attr("stock") || $btn.data("stock") || "0", 10);
2091        var n_cor     = $btn.attr("n_cor")     || $btn.data("n_cor")     || "";
2092        var n_tamanho = $btn.attr("n_tamanho") || $btn.data("n_tamanho") || "";
2093        var estado    = $btn.attr("estado")    || $btn.data("estado")    || ""; // "on" ou ""
2094
2095        $("#n_cor").val(n_cor);
2096        $("#n_tamanho").val(n_tamanho);
2097        $("#n_tamanho_box").val(n_tamanho);
2098        $("#fake_size").val(n_tamanho);
2099        $("#stock").val(stock);
2100
2101        $(".sizebox").removeClass("size_btn_active");
2102
2103        if (estado === "on" && stock !== 0 && stock !== -1) {
2104          $("#btn-shopcart-ficha").removeAttr("disabled")
2105                                  .removeClass("btn-shopcart-ficha-disabled");
2106          $btn.addClass("size_btn_active");
2107
2108          var opts = "";
2109          for (var i = 1; i <= stock; i++) {
2110            opts += '<option value="' + i + '">' + i + "</option>";
2111          }
2112          $("#stock_select").empty().append(opts);
2113
2114          $("#nosizeavailable-a").removeClass("geralOrange");
2115          $("#nosizeavailable-i").removeClass("geralOrangeIcon");
2116        } else {
2117          toastr.error(gl("ALERTA_TAMANHO_NAO_DISPONIVEL"));
2118          $("#btn-shopcart-ficha").addClass("btn-shopcart-ficha-disabled")
2119                                  .attr("disabled", "true");
2120          $(".sizebox").removeClass("size_btn_active");
2121          $("#nosizeavailable-a").addClass("geralOrange");
2122          $("#nosizeavailable-i").addClass("geralOrangeIcon");
2123        }
2124      });
2125  }
2126
2127function bindProductAddToCart() {
2128  $(document)
2129    .off("click.addFicha")
2130    .on("click.addFicha", "#btn-shopcart-ficha", function (e) {
2131      e.preventDefault();
2132
2133      var $btn     = $(this);
2134      var $spinner = $("#btn-shopcart-ficha-loading");
2135
2136      var id_code   = $("#id_code").val() || $btn.data("id_code") || $btn.attr("id_code");
2137      var n_cor     = $("#n_cor").val() || "";
2138      var n_tamanho = $("#n_tamanho").val() || "";
2139      var qty       = $("#stock_select").val() || 1;
2140      var sizeBox   = $("#n_tamanho_box").val() || "";
2141
2142      if (!id_code) return;
2143
2144      // validação de tamanho
2145      var $tabelaTamanhos = $("#tabela_tamanhos");
2146      if ($tabelaTamanhos.length && (!sizeBox || sizeBox === "0")) {
2147        if (typeof toastr !== "undefined") {
2148          toastr.warning(gl("ALERT_NOPRODS_CESTO") || "Selecione um tamanho antes de continuar.");
2149        }
2150        $tabelaTamanhos.addClass("tabela_tamanhos_alert");
2151        setTimeout(function () {
2152          $tabelaTamanhos.removeClass("tabela_tamanhos_alert");
2153        }, 9000);
2154        return;
2155      }
2156
2157      // ------------------------------------------------------------------
2158      // OFERTAS: ler dos inputs hidden (novo) ou reconstruir como fallback
2159      // ------------------------------------------------------------------
2160      var selectedOfferId  = $("#selected_offer_id").val() || null;
2161      var selectedOfferIds = $("#selected_offer_ids").val() || "";
2162
2163      if (!selectedOfferIds) {
2164        // fallback: construir a string id:qty|id2:qty2 a partir dos checkboxes
2165        var parts = [];
2166        $(".offering_items_container .cCheck:checked").each(function () {
2167          var $item = $(this).closest(".offering_item");
2168          if (!$item.length) return;
2169
2170          var oid = $item.data("id") || $item.attr("data-id");
2171          if (!oid) return;
2172
2173          var $sel = $item.find(".offer_qty_select");
2174          var q    = parseInt($sel.val(), 10);
2175          if (isNaN(q) || q < 1) q = 1;
2176
2177          parts.push(oid + ":" + q);
2178          if (!selectedOfferId) selectedOfferId = oid;
2179        });
2180
2181        if (parts.length) {
2182          selectedOfferIds = parts.join("|");
2183        }
2184      }
2185
2186      // ------------------------------------------------------------------
2187      // Preparar payload
2188      // ------------------------------------------------------------------
2189      $btn.prop("disabled", true).hide();
2190      $spinner.show();
2191
2192      var payload = {
2193        ajax: "shopcart_add",
2194        id_code: id_code,
2195        stock: qty,
2196        n_cor: n_cor,
2197        n_tamanho: n_tamanho,
2198        n_tamanho_box: sizeBox,
2199        fake_size: $("#fake_size").val() || "",
2200        qty: qty
2201      };
2202
2203      if (selectedOfferId) {
2204        payload.selected_offer_id = selectedOfferId;      // legacy
2205      }
2206      if (selectedOfferIds) {
2207        payload.selected_offer_ids = selectedOfferIds;    // novo formato id:qty|id2:qty2
2208      }
2209
2210      $.ajax({
2211        type: "POST",
2212        url: REQUEST_ENDPOINT,
2213        async: true,
2214        cache: false,
2215        data: payload
2216      })
2217        .done(function (resp) {
2218          var data = {};
2219          try {
2220            data = (typeof resp === "object") ? resp : JSON.parse(resp);
2221          } catch (err) {
2222            data = {};
2223          }
2224
2225          if (data.status === 1) {
2226            // GA4 / DataLayer
2227            if (data.item) {
2228              window.dataLayer = window.dataLayer || [];
2229              window.dataLayer.push({
2230                event: "add_to_cart",
2231                ecommerce: {
2232                  currency: data.item.currency || "EUR",
2233                  value: data.item.price,
2234                  items: [data.item]
2235                }
2236              });
2237            }
2238
2239            if (typeof window.check_shopcart === "function") window.check_shopcart();
2240            if (typeof t === "function") t(id_code);
2241
2242            $(".fichaGotoCart").fadeIn(600);
2243            $(".shopcart-selector-right, .shopcart-selector-left").addClass("flash");
2244            setTimeout(function () {
2245              $(".shopcart-selector-right, .shopcart-selector-left").removeClass("flash");
2246            }, 3000);
2247
2248            /* t1592: sem toast «Produto adicionado ao cesto!» — badge/flash do cesto bastam */
2249
2250          } else if (data.error === "maxsamples" || data.status === "maxsamples") {
2251            // compat: aceita error ou antigo status
2252            if (typeof Swal !== "undefined") {
2253              Swal.fire({
2254                title: gl("swal_attention") || "Atenção",
2255                text: gl("SAMPLES_MAXEDOUT") || "Atingiu o limite de amostras.",
2256                icon: "warning",
2257                confirmButtonColor: "#ABD559",
2258                confirmButtonText: gl("swal_btnok") || "OK"
2259              });
2260            } else if (typeof toastr !== "undefined") {
2261              toastr.warning(gl("SAMPLES_MAXEDOUT") || "Atingiu o limite de amostras.");
2262            }
2263
2264          } else {
2265            var message = data.message || gl("ALERT_NOPRODS_CESTO") || "Não foi possível adicionar ao carrinho.";
2266            if (typeof Swal !== "undefined") {
2267              Swal.fire(gl("swal_attention") || "Atenção", message, "warning");
2268            } else if (typeof toastr !== "undefined") {
2269              toastr.error(message);
2270            }
2271          }
2272        })
2273        .fail(function () {
2274          if (typeof toastr !== "undefined") {
2275            toastr.error(gl("ALERT_NOPRODS_CESTO") || "Não foi possível adicionar ao carrinho.");
2276          }
2277        })
2278        .always(function () {
2279          $spinner.hide();
2280          $btn.show().prop("disabled", false);
2281        });
2282    });
2283}
2284
2285 
2286
2287  function bindProductTabsNavigation() {
2288    $(document)
2289      .off("click.navT")
2290      .on("click.navT", ".navigationT, .tabsT a", function (e) {
2291        e.preventDefault();
2292        var $link = $(this);
2293        var targetId = ($link.attr("href") || $link.data("target") || $link.attr("id") || "").replace(/^#/, "");
2294        if (!targetId) return;
2295
2296        $(".tabsT a, .navigationT").removeClass("selected");
2297        $link.addClass("selected");
2298
2299        $(".tabInfoT .tabT").hide();
2300        $(".tabInfoT ." + targetId).fadeIn("fast");
2301      });
2302  }
2303
2304  // ===== alterar quantidade no carrinho =====
2305  function bindCartQuantityControls() {
2306    $(document).off("change.cartQty").on("change.cartQty", "#stock_shopcart, .stock_shopcart_element", function () {
2307      var $select = $(this);
2308
2309      var id_code = $select.attr("id_code") || $select.data("id_code");
2310      var element_id_code = $select.attr("element_id_code") || $select.data("element_id_code");
2311      var current_qtd = $select.attr("current_qtd") || 1;
2312      var new_qtd = $select.val();
2313
2314      $.ajax({
2315        type: "POST",
2316        url: REQUEST_ENDPOINT,
2317        data: {
2318          ajax: "shopcart_change_qtd",
2319          id_code: id_code,
2320          element_id_code: element_id_code,
2321          current_qtd: current_qtd,
2322          new_qtd: new_qtd
2323        }
2324      }).done(function () {
2325        var reloadType = $("#delivery_methods").is(":visible") ? "shopcart_remove" : "html";
2326        if (typeof generate_shopcart === "function") generate_shopcart(reloadType);
2327        if (typeof check_shopcart === "function") check_shopcart();
2328        if ($(".checkout-modern").length && typeof window.refreshCheckoutProductsCard === "function") {
2329          window.refreshCheckoutProductsCard({ refreshLists: false });
2330        }
2331        if (typeof sendCheckoutEvent === "function") {
2332          sendCheckoutEvent(2, "checkout_progress");
2333        }
2334      }).fail(function () {
2335        if (typeof toastr !== "undefined") {
2336          toastr.error(gl("ALERT_NOPRODS_CESTO") || "Não foi possível atualizar a quantidade.");
2337        } else {
2338          console.error("shopcart_change_qtd failed");
2339        }
2340      });
2341    });
2342  }
2343
2344  function bindDiscountHandlers() {
2345    $(document)
2346      .off("click.applyDiscount")
2347      .on("click.applyDiscount", "#apply_discount", function (e) {
2348        e.preventDefault();
2349
2350        var $button = $(this);
2351        var $scope = $button.closest(".checkout-modern__promo-form, #shopcart_discount_html");
2352        var $input = $scope.find("#discount_code").first();
2353        if (!$input.length) $input = $(".checkout-modern__cart-aside #discount_code").first();
2354        if (!$input.length) $input = $("#discount_code").last();
2355
2356        if ($button.hasClass("is-removing-discount") || $.trim($button.val()).toUpperCase() === "REMOVER") {
2357          $("#discount_code").val("");
2358          $button.removeData("appliedCode").val("APLICAR").removeClass("is-removing-discount");
2359          if (typeof generate_shopcart === "function") generate_shopcart("html", "");
2360          if (typeof generate_shopcart_discounts === "function") generate_shopcart_discounts("");
2361          if (typeof populate_shopcart === "function") populate_shopcart();
2362          return;
2363        }
2364
2365        var code = $.trim($input.val() || "");
2366        if (!code) {
2367          if (typeof Swal !== "undefined") {
2368            Swal.fire("Atenção", "Por favor introduza um código de desconto.", "info");
2369          } else if (typeof toastr !== "undefined") {
2370            toastr.warning("Por favor introduza um código de desconto.");
2371          }
2372          return;
2373        }
2374
2375        $.ajax({
2376          type: "POST",
2377          url: REQUEST_ENDPOINT,
2378          cache: false,
2379          data: { ajax: "validate_discount_code", discount_code: code }
2380        }).done(function (resp) {
2381          var clean = (resp || "").toString().trim();
2382          var isValid = !isNaN(clean) && parseFloat(clean) > 0;
2383
2384          if (isValid) {
2385            $("#discount_code").val(code);
2386            $button.data("appliedCode", code).val("REMOVER").addClass("is-removing-discount");
2387            if (typeof Swal !== "undefined") {
2388              Swal.fire("Desconto aplicado", "O seu código foi aplicado com sucesso.", "success");
2389            } else if (typeof toastr !== "undefined") {
2390              toastr.success("Desconto aplicado com sucesso.");
2391            }
2392
2393            if (typeof generate_shopcart === "function") generate_shopcart("html", code);
2394            if (typeof generate_shopcart_discounts === "function") generate_shopcart_discounts(code);
2395            if (typeof populate_shopcart === "function") populate_shopcart();
2396          } else {
2397            if (typeof Swal !== "undefined") {
2398              Swal.fire("Desconto inválido", "O código que introduziu não é válido.", "error");
2399            } else if (typeof toastr !== "undefined") {
2400              toastr.error("Código de desconto inválido.");
2401            }
2402
2403            if (typeof generate_shopcart_discounts === "function") generate_shopcart_discounts("");
2404            $("#discount_code").val("");
2405            $button.removeData("appliedCode").val("APLICAR").removeClass("is-removing-discount");
2406          }
2407        }).fail(function () {
2408          toastr.error("Não foi possível validar o código de desconto.");
2409        });
2410      });
2411
2412    $(document)
2413      .off("change.discountCodeToggle")
2414      .on("change.discountCodeToggle", "#discount_code_checkbox", function () {
2415        if (this.checked) {
2416          var code = $("#discount_code").val();
2417          if (code) $("#apply_discount").trigger("click");
2418          return;
2419        }
2420
2421        $.post(REQUEST_ENDPOINT, { ajax: "validate_discount_code", discount_code: "" }, function () {
2422          if (typeof generate_shopcart === "function") generate_shopcart("html");
2423          if (typeof generate_shopcart_discounts === "function") generate_shopcart_discounts("");
2424
2425          if (typeof Swal !== "undefined") {
2426            Swal.fire({
2427              toast: true,
2428              position: "top-end",
2429              icon: "info",
2430              title: "Desconto removido",
2431              showConfirmButton: false,
2432              timer: 2000,
2433              timerProgressBar: true
2434            });
2435          } else if (typeof toastr !== "undefined") {
2436            toastr.info("Desconto removido.");
2437          }
2438        });
2439      });
2440  }
2441
2442// ===== FINALIZAR ENCOMENDA =====
2443  function bindCheckoutFlowEvents() {
2444    $(document)
2445      .off("click.shopcartCheckout")
2446      .on("click.shopcartCheckout", "#shopcart_checkout_button", function (e) {
2447        e.preventDefault();
2448
2449        var hasItems = 1;
2450        if ($("#check_amostras").length) {
2451          hasItems = parseInt($("#check_amostras").val(), 10) || 0;
2452        }
2453        if (hasItems < 1) {
2454          var samplesOnly = $("#check_amostras").attr("data-samples-only") === "1";
2455          var emptyMsg = samplesOnly
2456            ? (gl("CV2_SAMPLES_ONLY_CHECKOUT") || "Não é possível finalizar uma encomenda só com amostras. Adicione pelo menos um produto da loja.")
2457            : (gl("COMPRAS_A_ZERO") || "O seu carrinho está vazio.");
2458          if (typeof Swal !== "undefined") {
2459            Swal.fire({
2460              title: "",
2461              text: emptyMsg,
2462              icon: "info",
2463              confirmButtonText: gl("swal_btnok") || "OK"
2464            });
2465          } else if (typeof toastr !== "undefined") {
2466            toastr.warning(emptyMsg);
2467          } else {
2468            alert(emptyMsg);
2469          }
2470          return;
2471        }
2472
2473        try {
2474          window.dataLayer = window.dataLayer || [];
2475          window.dataLayer.push({
2476            event: "gtm_begin_checkout",
2477            event_category: "ecommerce",
2478            event_label: "Finalizar encomenda"
2479          });
2480        } catch (err) {}
2481
2482        if (typeof toastr !== "undefined") toastr.clear();
2483        $("#shopcart_option_buttons").stop(true, true).slideUp(150, function () {
2484          $("#shopcart_option_buttons").hide();
2485        });
2486        $("#footer_container").stop(true, true).fadeOut(150);
2487
2488        if (typeof check_shopcart === "function") check_shopcart("init");
2489        if (typeof populate_shopcart === "function") populate_shopcart();
2490        if (typeof shopcart_specialservices === "function") shopcart_specialservices();
2491        if (typeof generate_shopcart === "function") generate_shopcart("html");
2492
2493        $.post(REQUEST_ENDPOINT, { ajax: "generate_shopcart", type: "html" }, function (html) {
2494          var $desktopPanel = $("#shopcart-modal-container");
2495          var $mobilePanel  = $("#mini-cart");
2496
2497          if ($desktopPanel.length) {
2498            if ($("#shopcart-modal").length) $("#shopcart-modal").html(html);
2499            else $desktopPanel.html(html);
2500          }
2501          if ($mobilePanel.length) $mobilePanel.html(html);
2502        });
2503
2504        $("#encomenda").fadeIn(300);
2505        var $enc = $("#encomenda");
2506        if ($enc.length) {
2507          $("html, body").animate({ scrollTop: $enc.offset().top - 80 }, 400);
2508        }
2509      });
2510
2511    $(document)
2512      .off("click.shopcartBack")
2513      .on("click.shopcartBack", "#shopcart_shopping_button", function (e) {
2514        e.preventDefault();
2515
2516        $.ajax({
2517          type: "POST",
2518          url: REQUEST_ENDPOINT,
2519          cache: false,
2520          data: { ajax: "getReferralByProduct", idcode: $("#latest_idcode").val() }
2521        }).done(function (url) {
2522          if (typeof url === "string" && url.trim()) {
2523            window.location.href = url.trim();
2524          }
2525        });
2526      });
2527
2528    $(document)
2529      .off("click.shopcartDelivery")
2530      .on("click.shopcartDelivery", ".shopcart_delivery_button", function (e) {
2531        e.preventDefault();
2532        $("#special_service").val("");
2533        if (typeof shopcart_specialservices === "function") shopcart_specialservices();
2534        clickDeliveryMethod($(this).attr("id"));
2535        check_shopcart();
2536      });
2537
2538    $(document)
2539      .off("click.shopcartPaytype")
2540      .on("click.shopcartPaytype", ".shopcart_paytype_button", function (e) {
2541        e.preventDefault();
2542        clickPayMethod($(this).attr("id"));
2543        check_shopcart();
2544      });
2545
2546    $(document)
2547      .off("change.shopcartCountry")
2548      .on("change.shopcartCountry", "#country, #delivery_country", function () {
2549        $("#special_service").val("");
2550        if (typeof shopcart_specialservices === "function") shopcart_specialservices();
2551        // CV2: sincronizar o pais guest com a sidebar ANTES dos populates
2552        if (this.id === "country" && window.cv2SetCountry && !($("#uid").val() > 0)) window.cv2SetCountry($(this).val());
2553        check_shopcart();
2554        // CV2: recalcular portes/totais (check_shopcart so atualiza o header)
2555        if (typeof populate_shopcart === "function") populate_shopcart();
2556      });
2557  }
2558
2559  function bindGiftAndInfoToggles() {
2560    $(document)
2561      .off("click.scInfoToggle")
2562      .on("click.scInfoToggle", "#sc_information_text", function (e) {
2563        e.preventDefault();
2564        $("#sc_information_text_container").toggleClass("fold", 500);
2565      });
2566
2567    $(document)
2568      .off("change.shopcartGift")
2569      .on("change.shopcartGift", "#send_as_gift", function () {
2570        populate_shopcart("send_as_gift");
2571      });
2572  }
2573
2574  function bindLiveSearchEvents() {
2575    $(document)
2576      .off("scroll.livesearchResults")
2577      .on("scroll.livesearchResults", ".livesearch-results", function () {
2578        if ($("#livesearch").css("display") === "block" && $("#q").is(":focus")) {
2579          $("#q").blur();
2580        }
2581        if ($(this).scrollTop() === 0) {
2582          $("#q").focus();
2583        }
2584      });
2585
2586    $(document)
2587      .off("keyup.livesearch keypress.livesearch click.livesearch", "#q.livesearch-input")
2588      .on("keyup.livesearch keypress.livesearch click.livesearch", "#q.livesearch-input", function (evt) {
2589        if (window.CV2_SEARCH_OVERLAY) return;
2590        var $input = $(this);
2591        $("#livesearch").scrollTop(0);
2592        var query = $input.val();
2593        var len = query.length;
2594        var key = evt.keyCode || evt.which;
2595
2596        if (len > 3) {
2597          if (key === 13) { evt.preventDefault(); return false; }
2598          if (key === 8) toggleLiveSearch("show");
2599          if (key !== 27 && key !== 1 && key !== 8 && key !== 32) {
2600            $.ajax({
2601              type: "POST",
2602              cache: true,
2603              url: "/livesearch.php",
2604              data: { q: query },
2605              timeout: 2000
2606            }).done(function (html) {
2607              toggleLiveSearch("show");
2608              $("#livesearch").html(html);
2609            });
2610          }
2611        } else {
2612          toggleLiveSearch("reset");
2613        }
2614
2615        $input.focus();
2616      });
2617
2618    $(document)
2619      .off("click.livesearchProduct")
2620      .on("click.livesearchProduct", ".livesearch-product-href", function (e) {
2621        var href = $(this).attr("href");
2622        if (href) {
2623          e.preventDefault();
2624          window.location.assign(href);
2625        }
2626      });
2627
2628    $(document)
2629      .off("click.livesearchReset")
2630      .on("click.livesearchReset", "#livesearch-reset, .livesearch-noresults-container", function () {
2631        $("html").css({ overflow: "auto" });
2632        toggleLiveSearch("hide");
2633        $("body").animate({ scrollTop: 0 }, 200);
2634      });
2635
2636    $(document)
2637      .off("click.livesearchGoUp")
2638      .on("click.livesearchGoUp", "#goUp", function (e) {
2639        e.preventDefault();
2640        $("body").animate({ scrollTop: 0 }, 200);
2641        $("#livesearch").animate({ scrollTop: 0 }, 200);
2642      });
2643
2644    $(document)
2645      .off("click.livesearchFavAdd")
2646      .on("click.livesearchFavAdd", ".livesearch_favorite_add", function (e) {
2647        e.preventDefault();
2648        var id = $(this).attr("id_code") || $(this).data("id_code");
2649        if (!id) return;
2650
2651        var $icon = $(this);
2652        $.post(REQUEST_ENDPOINT, { ajax: "shopcart_favorites_add", id_code: id }, () => {
2653          $icon
2654            .removeClass("livesearch_favorite_add fa-heart-o far")
2655            .addClass("livesearch_favorite_remove fa fa-heart");
2656          toastr.info(gl("LIVES_ADD_FAVORITE_INFO") || "Adicionado aos favoritos");
2657        });
2658      });
2659
2660    $(document)
2661      .off("click.livesearchFavRemove")
2662      .on("click.livesearchFavRemove", ".livesearch_favorite_remove", function (e) {
2663        e.preventDefault();
2664        var id = $(this).attr("id_code") || $(this).data("id_code");
2665        if (!id) return;
2666
2667        var $icon = $(this);
2668        $.post(REQUEST_ENDPOINT, { ajax: "shopcart_favorites_remove", id_code: id }, () => {
2669          $icon
2670            .removeClass("livesearch_favorite_remove fa")
2671            .addClass("livesearch_favorite_add far fa-heart-o");
2672          toastr.warning(gl("LIVES_REM_FAVORITE_INFO") || "Removido dos favoritos");
2673        });
2674      });
2675  }
2676
2677  function bindDeliveryApiLanguageSwitch() {
2678    $(document)
2679      .off("click.cttLangPt")
2680      .on("click.cttLangPt", "#ctt_lang_pt", function () {
2681        // sem preventDefault: impedia o radio de ficar checked (estado do pais nunca mudava)
2682        this.checked = true;
2683        callAPIresults("pt");
2684      });
2685
2686    $(document)
2687      .off("click.cttLangEs")
2688      .on("click.cttLangEs", "#ctt_lang_es", function () {
2689        this.checked = true;
2690        callAPIresults("es");
2691      });
2692  }
2693
2694  function shopcartDrawerEscapeHtml(value) {
2695    return String(value == null ? "" : value)
2696      .replace(/&/g, "&amp;")
2697      .replace(/</g, "&lt;")
2698      .replace(/>/g, "&gt;")
2699      .replace(/"/g, "&quot;")
2700      .replace(/\x27/g, "&#039;");
2701  }
2702
2703  function closeShopcartRemoveModal() {
2704    $(".cupido-cart-remove-scrim").remove();
2705    $(document).off("keyup.shopcartRemoveModal");
2706    $("body").removeClass("cupido-cart-remove-modal-open");
2707  }
2708
2709  function shopcartSavedForLaterAvailable() {
2710    var uid = parseInt($("#session_uid").val() || "0", 10);
2711    return uid > 0;
2712  }
2713
2714  function getShopcartRemoveModalProduct($button) {
2715    var $item = $button.closest(".shopcart-drawer__item");
2716    var $row = $button.closest("#shopcart_html .shop_cart_table tr");
2717    var $title = $item.length ? $item.find(".shopcart-drawer__item-title").first().clone() : $row.children("td").eq(2).clone();
2718    var $name = $title.find("a").first();
2719    var imgSrc = $item.length ? ($item.find(".shopcart-drawer__thumb img").first().attr("src") || "") : ($row.find("img").first().attr("src") || "");
2720    var href = $name.attr("href") || "#";
2721    var name = $.trim(($name.text() || $title.text() || "").replace(/\s+/g, " "));
2722    var quantity = ($item.length ? $item.find(".shopcart-drawer__qty-select").first().val() : $row.find("select").first().val()) || "1";
2723    var price = $.trim(($item.length ? $item.find(".shopcart-drawer__price-total").first().text() : $row.find(".checkout-cart-line-total").first().text()) || "");
2724
2725    return {
2726      image: imgSrc,
2727      href: href,
2728      name: name,
2729      meta: quantity + " " + (gl("SHOPCART_REMOVE_MODAL_UNIT") || "un.") + (price ? " · " + price : "")
2730    };
2731  }
2732
2733  function openShopcartRemoveModal($button, key, eKey, removeItem) {
2734    var product = getShopcartRemoveModalProduct($button);
2735    var title = gl("SHOPCART_REMOVE_MODAL_TITLE") || "Remover do cesto?";
2736    var message = gl("SHOPCART_SAVE_FOR_LATER_MESSAGE") || "Guarde-o nos favoritos para o encontrar mais tarde.";
2737    var saveEnabled = shopcartSavedForLaterAvailable();
2738    var saveLabel = gl("SHOPCART_SAVE_FOR_LATER") || "Guardar para depois";
2739    var saveMessageHtml = saveEnabled ? "<p class=\"pv-modal__text pv-modal__text--cart-remove\">" + shopcartDrawerEscapeHtml(message) + "</p>" : "";
2740    var saveActionHtml = saveEnabled ? "<button type=\"button\" class=\"cup-cart-remove-btn cup-cart-remove-btn--link\" data-cart-remove-save>" + shopcartDrawerEscapeHtml(saveLabel) + "</button>" : "<span class=\"cup-cart-remove-spacer\"></span>";
2741    var cancelLabel = gl("swal_btncancel") || "Cancelar";
2742    var removeLabel = gl("SHOPCART_REMOVE") || gl("swal_btnyes") || "Remover";
2743    var productHtml = [
2744      "<a class=\"pv-modal-product\" href=\"", shopcartDrawerEscapeHtml(product.href), "\">",
2745      "<span class=\"pv-modal-product__image\">",
2746      product.image ? "<img src=\"" + shopcartDrawerEscapeHtml(product.image) + "\" alt=\"" + shopcartDrawerEscapeHtml(product.name) + "\">" : "",
2747      "</span>",
2748      "<span class=\"pv-modal-product__content\">",
2749      "<strong class=\"pv-modal-product__name\">", shopcartDrawerEscapeHtml(product.name), "</strong>",
2750      "<span class=\"pv-modal-product__meta\">", shopcartDrawerEscapeHtml(product.meta), "</span>",
2751      "</span>",
2752      "</a>"
2753    ].join("");
2754
2755    closeShopcartRemoveModal();
2756    $("body").addClass("cupido-cart-remove-modal-open").append([
2757      "<div class=\"pv-scrim cupido-cart-remove-scrim\" role=\"presentation\">",
2758      "<div class=\"pv-modal pv-modal--cart-remove\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"cupido-cart-remove-title\">",
2759      "<button type=\"button\" class=\"pv-modal__close\" data-cart-remove-close aria-label=\"Fechar\">&times;</button>",
2760      "<div class=\"pv-modal__head\"><h3 id=\"cupido-cart-remove-title\" class=\"pv-modal__title\">", shopcartDrawerEscapeHtml(title), "</h3></div>",
2761      "<div class=\"pv-modal__body\">", productHtml, saveMessageHtml, "</div>",
2762      "<div class=\"pv-modal__foot pv-modal__foot--cart-remove\">",
2763      saveActionHtml,
2764      "<span class=\"pv-modal__foot-actions\">",
2765      "<button type=\"button\" class=\"cup-cart-remove-btn cup-cart-remove-btn--ghost\" data-cart-remove-close>", shopcartDrawerEscapeHtml(cancelLabel), "</button>",
2766      "<button type=\"button\" class=\"cup-cart-remove-btn cup-cart-remove-btn--danger\" data-cart-remove-confirm>", shopcartDrawerEscapeHtml(removeLabel), "</button>",
2767      "</span>",
2768      "</div>",
2769      "</div>",
2770      "</div>"
2771    ].join(""));
2772
2773    $(document).on("keyup.shopcartRemoveModal", function (event) {
2774      if (event.key === "Escape" || event.keyCode === 27) closeShopcartRemoveModal();
2775    });
2776
2777    $(".cupido-cart-remove-scrim")
2778      .off("click.shopcartRemoveModal")
2779      .on("click.shopcartRemoveModal", function (event) {
2780        if ($(event.target).is(".cupido-cart-remove-scrim, [data-cart-remove-close]")) {
2781          event.preventDefault();
2782          closeShopcartRemoveModal();
2783        }
2784      })
2785      .on("click.shopcartRemoveModal", "[data-cart-remove-confirm]", function (event) {
2786        event.preventDefault();
2787        closeShopcartRemoveModal();
2788        removeItem();
2789      })
2790      .on("click.shopcartRemoveModal", "[data-cart-remove-save]", function (event) {
2791        event.preventDefault();
2792        var finish = function () {
2793          closeShopcartRemoveModal();
2794          removeItem();
2795        };
2796
2797        if (key) {
2798          $.post(REQUEST_ENDPOINT, { ajax: "shopcart_saved_for_later_add", id_code: key, id_produto_elemento: eKey || 0 }, function (response) {
2799            var data = {};
2800            try { data = (typeof response === "object") ? response : JSON.parse(response); } catch (err) { data = {}; }
2801            if (data.ok) finish();
2802          });
2803        }
2804      });
2805  }
2806
2807  function bindCartRemovalEvents() {
2808    $(document)
2809      .off("click.shopcartRemove")
2810      .on("click.shopcartRemove", ".shopcart_remove", function (e) {
2811        e.preventDefault();
2812        e.stopPropagation();
2813        var key = $(this).attr("key");
2814        var eKey = $(this).attr("e_key");
2815        if (!key && !eKey) return;
2816
2817        var removeItem = function () {
2818          var drawerItems = $(".shopcart-drawer__item").length;
2819          var tableRemoves = $("#shopcart_html .shopcart_remove").length;
2820          if ((drawerItems > 0 && drawerItems <= 1) || (tableRemoves > 0 && tableRemoves <= 1)) {
2821            window.__cv2UserEmptiedCart = true;
2822          }
2823          window.__cv2CartRemoving = true;
2824          if (typeof window.setShopcartDrawerLoading === "function") window.setShopcartDrawerLoading(true);
2825          $.ajax({
2826            type: "POST",
2827            url: REQUEST_ENDPOINT,
2828            cache: false,
2829            data: { ajax: "shopcart_remove", key: key, e_key: eKey }
2830          }).done(function (response) {
2831            var res = {};
2832            if (typeof response === "string") {
2833              try { res = JSON.parse(response); } catch (err) { res = {}; }
2834            } else if (typeof response === "object") {
2835              res = response || {};
2836            }
2837
2838            if (res.status === 1 && res.item) {
2839              window.dataLayer = window.dataLayer || [];
2840              window.dataLayer.push({
2841                event: "remove_from_cart",
2842                currency: "EUR",
2843                items: [res.item]
2844              });
2845            }
2846
2847            if ($("body").hasClass("shopcart-drawer-open") && typeof window.openShopcartDrawer === "function") {
2848              window.openShopcartDrawer();
2849            }
2850            if ($("#shopcart_html").length) {
2851              refreshCheckoutAfterCartChange();
2852            } else if (!$("body").hasClass("shopcart-drawer-open")) {
2853              window.location.reload();
2854            }
2855          }).always(function () {
2856            window.__cv2CartRemoving = false;
2857            if (typeof window.setShopcartDrawerLoading === "function") window.setShopcartDrawerLoading(false);
2858          });
2859        };
2860
2861        if ($(this).closest(".shopcart-drawer__item").length || $(this).closest("#shopcart_html .shop_cart_table").length) {
2862          openShopcartRemoveModal($(this), key, eKey, removeItem);
2863          return;
2864        }
2865
2866        if (typeof swal === "function") {
2867          swal({
2868            title: gl("swal_attention") || "Atenção",
2869            text: gl("swal_action_will_delete_item") || "Deseja remover este item?",
2870            type: "warning",
2871            confirmButtonColor: "#ABD559",
2872            showCancelButton: true,
2873            animation: false,
2874            confirmButtonText: gl("swal_btnyes") || "Sim",
2875            cancelButtonText: gl("swal_btncancel") || "Cancelar",
2876            closeOnConfirm: true
2877          }, function (confirmed) {
2878            if (confirmed) removeItem();
2879          });
2880        } else if (confirm(gl("swal_action_will_delete_item") || "Remover este item?")) {
2881          removeItem();
2882        }
2883      });
2884  }
2885
2886  function enableMobileStickyNav() {
2887    var navWrapper = document.querySelector(".catalog-top-wrapper");
2888    var sentinel = document.getElementById("catalog_top");
2889    if (!navWrapper || !sentinel) return;
2890
2891    var togglePinned = function (isPinned) {
2892      navWrapper.classList.toggle("catalog-top-wrapper--pinned", !!isPinned);
2893      document.body.classList.toggle("catalog-sticky-active", !!isPinned);
2894    };
2895
2896    if ("IntersectionObserver" in window) {
2897      var observer = new IntersectionObserver(
2898        function (entries) {
2899          var entry = entries[0];
2900          togglePinned(entry.intersectionRatio === 0 && entry.boundingClientRect.top < 0);
2901        },
2902        { threshold: [0, 1] }
2903      );
2904      observer.observe(sentinel);
2905    } else {
2906      var sentinelOffset = sentinel.getBoundingClientRect().top + window.pageYOffset;
2907      var onScroll = function () {
2908        togglePinned(window.pageYOffset > sentinelOffset);
2909      };
2910      window.addEventListener("scroll", onScroll);
2911      onScroll();
2912    }
2913  }
2914
2915
2916
2917	/* ===== Tools Popover (Língua & Câmbio) ===== */
2918	function bindToolsPopover() {
2919  const $btn = $("#tools-container");
2920  const $pop = $("#tools-options");
2921  if (!$btn.length || !$pop.length) return;
2922
2923  // Move para o body apenas uma vez
2924  if (!$pop.data("ported")) {
2925    $pop
2926      .appendTo("body")
2927      .data("ported", true)
2928      .hide()
2929      .css({
2930        position: "absolute",
2931        zIndex: 9999,
2932        width: 165,          // fixa a largura máxima desejada
2933        maxWidth: 165,       // impede que cresça
2934        minWidth: 165,       // mantém consistência
2935      })
2936      .addClass("tools-popover");
2937  }
2938
2939	function place() {
2940		const rect = $btn[0].getBoundingClientRect();
2941		const scrollTop  = window.pageYOffset  || document.documentElement.scrollTop;
2942		const scrollLeft = window.pageXOffset  || document.documentElement.scrollLeft;
2943
2944		const btnW = rect.width;
2945		const btnH = rect.height;
2946
2947		const popW = 162;
2948		$pop.css({ width: popW, maxWidth: popW, minWidth: popW, margin: 0 });
2949
2950		// alinhado pela borda direita + pequeno espaçamento
2951		const left = rect.left + scrollLeft + btnW - popW + 8;
2952		const top  = rect.top  + scrollTop  + btnH + 8;
2953
2954		// (sem clamping automático)
2955		$pop.css({ top: top, left: left });
2956	}
2957
2958  function show() {
2959    place();
2960    $("#shopcart-modal-container").hide();
2961    $pop.stop(true, true).fadeIn(100);
2962  }
2963
2964  function hide() {
2965    $pop.stop(true, true).fadeOut(80);
2966  }
2967
2968  // Limpa handlers antigos
2969  $(document).off(".toolsPopover");
2970  $btn.off(".toolsPopover");
2971  $pop.off(".toolsPopover");
2972
2973  // Click para abrir/fechar
2974  $btn.on("click.toolsPopover", function (e) {
2975    e.preventDefault();
2976    e.stopPropagation();
2977    $pop.is(":visible") ? hide() : show();
2978  });
2979
2980  // Fecha ao clicar fora
2981  $(document).on("click.toolsPopover", function (e) {
2982    if (!$(e.target).closest("#tools-options, #tools-container").length)
2983      hide();
2984  });
2985
2986		// Reposiciona se estiver aberto
2987		$(window).on("scroll.toolsPopover resize.toolsPopover", function () {
2988			if ($pop.is(":visible")) place();
2989		});
2990	}
2991
2992	bindToolsPopover();
2993
2994  /* ==========================================================
2995   * 6) PESQUISA
2996   * ========================================================== */
2997  (function () {
2998    var $q = $("#q"); if ($q.length) {
2999      $q.on("keydown", function (e) {
3000        if (e.key === "Enter") {
3001          e.preventDefault();
3002          var t = $.trim($q.val() || ""); if (!t) return;
3003          var $f = $q.closest("form"); if ($f.length) $f.trigger("submit");
3004          else location.href = "/search/?q=" + encodeURIComponent(t);
3005        }
3006      });
3007    }
3008  })();
3009
3010  /* ==========================================================
3011   * 7) DATA LAYER / ANALYTICS E OUTROS HANDLERS GERAIS
3012   * ========================================================== */
3013  $(function () {
3014    // Fancybox básico + alert toggle
3015    if ($.fancybox) $(".fancybox").fancybox();
3016    $(document).on("click.ux", ".alert .toggle-alert", function () {
3017      $(this).closest(".alert").slideUp(); return false;
3018    });
3019
3020    // Lazy
3021    if ($.fn.lazy) {
3022      $(".lazy").Lazy({ scrollDirection: "vertical", effect: "fadeIn", effectTime: 500, threshold: 0, visibleOnly: true });
3023      if ($.fn.lazy) $(".lazy-throttle").lazy({ enableThrottle: true, throttle: 250, effect: "fadeIn", effectTime: 500, threshold: 0 });
3024    }
3025
3026    // Botões de evento
3027    $(".btnGreenLargeSH").on("click", function () {
3028      window.dataLayer = window.dataLayer || [];
3029      window.dataLayer.push({ event: 'begin_checkout', event_category: 'ecommerce', event_label: 'Checkout iniciado' });
3030    });
3031    $("#shopcart_shopping_button").on("click", function () {
3032      window.dataLayer = window.dataLayer || [];
3033      window.dataLayer.push({ event: 'continue_shopping', event_category: 'ecommerce', event_label: 'Botão continuar a comprar' });
3034    });
3035
3036    // Scroll depth (25/50/75/100)
3037    (function () {
3038      window.dataLayer = window.dataLayer || [];
3039      var thresholds = [25, 50, 75, 100], triggered = {};
3040      function throttle(fn, delay) { var last = 0; return function () { var now = Date.now(); if (now - last < delay) return; last = now; fn.apply(this, arguments); }; }
3041      function onScroll() {
3042        var y = window.scrollY || window.pageYOffset;
3043        var doc = Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
3044        var wh = window.innerHeight;
3045        var p = Math.round((y / (doc - wh)) * 100);
3046        thresholds.forEach(function (t) {
3047          if (p >= t && !triggered[t]) { triggered[t] = 1; window.dataLayer.push({ event: 'scroll', scroll_threshold: t }); }
3048        });
3049      }
3050      window.addEventListener('scroll', throttle(onScroll, 200));
3051    })();
3052  });
3053
3054  /* ==========================================================
3055   * 8) LOGIN / GOOGLE OAUTH / MISC LEGADO
3056   * ========================================================== */
3057  function generate_cupidovip(uid) {
3058    if (!uid) return;
3059
3060    $.ajax({
3061      type: "POST",
3062      url: REQUEST_ENDPOINT,
3063      cache: false,
3064      data: {
3065        ajax: "shopcart_cupidovip",
3066        uid: uid
3067      }
3068    }).done(function (html) {
3069      $("#shopcart_login, #account_login, #shop_cart_table_form").hide();
3070      $("#shopcart_cupidovip").html(html).fadeIn(700);
3071      try {
3072        if (typeof window.cv2MaybeOpenIncompleteAccount === "function") {
3073          window.setTimeout(window.cv2MaybeOpenIncompleteAccount, 80);
3074          window.setTimeout(window.cv2MaybeOpenIncompleteAccount, 400);
3075        } else if ($("#cacc_form").length) {
3076          var addBtns = $("#cacc_view .checkout-account__field--phone [data-cacc-edit], #cacc_view .checkout-account__field--addr [data-cacc-edit]");
3077          if (addBtns.length) {
3078            $("#cacc_view").hide();
3079            $("#cacc_form").show();
3080          }
3081        }
3082      } catch (eVipOpen) {}
3083    });
3084  }
3085  window.generate_cupidovip = generate_cupidovip;
3086
3087  function logoutUser() {
3088    $.post(REQUEST_ENDPOINT, { ajax: "logout" }, function (res) {
3089      var r;
3090      try { r = typeof res === "object" ? res : JSON.parse(res); }
3091      catch (err) { r = {}; }
3092
3093      if (r && r.googleLogout) {
3094        var logoutUrl = "https://accounts.google.com/Logout";
3095        var backToSite = "https://www.cupidosshop.com/logout.php";
3096
3097        var iframe = document.createElement("iframe");
3098        iframe.style.display = "none";
3099        iframe.src = logoutUrl;
3100        document.body.appendChild(iframe);
3101
3102        setTimeout(function () {
3103          window.location.href = backToSite;
3104        }, 500);
3105      } else {
3106        window.location.reload();
3107      }
3108    });
3109  }
3110  window.logout = logoutUser;
3111
3112  function cttBuildExpedicao() {
3113    $.post(REQUEST_ENDPOINT, { ajax: "cttBuildExpedicao" });
3114  }
3115
3116  var accountRecaptchaRendered = false;
3117  var accountRecaptchaTries = 0;
3118
3119  function ensureAccountRecaptcha() {
3120    var $container = $("#g-recaptcha");
3121    if (!$container.length) return;
3122    if (accountRecaptchaRendered) return;
3123    if (typeof grecaptcha === "undefined" || !grecaptcha.render) {
3124      if (accountRecaptchaTries < 20) {
3125        accountRecaptchaTries++;
3126        setTimeout(ensureAccountRecaptcha, 500);
3127      }
3128      return;
3129    }
3130
3131    grecaptcha.render("g-recaptcha", {
3132      sitekey: "6LetpiEUAAAAAOrCNcPBkwobhVBEjjhAi9IkEYVr",
3133      callback: function (token) {
3134        $("#g-recaptcha-response").val(token);
3135      }
3136    });
3137    accountRecaptchaRendered = true;
3138  }
3139
3140  function populateAccountForm() {
3141    if (!$("#account_table_form").length) return;
3142    $.ajax({
3143      type: "POST",
3144      url: REQUEST_ENDPOINT,
3145      cache: false,
3146      data: { ajax: "getUserData" }
3147    }).done(function (resp) {
3148      var data = {};
3149      try { data = typeof resp === "object" ? resp : JSON.parse(resp); }
3150      catch (err) { data = {}; }
3151
3152      $.each(data, function (key, value) {
3153        var $el = $("#" + key);
3154        if (!$el.length) return;
3155
3156        if ($el.is(":checkbox")) {
3157          var isChecked = (value === "on" || value === "1" || value === 1 || value === true);
3158          $el.prop("checked", isChecked);
3159        } else {
3160          $el.val(value);
3161        }
3162      });
3163
3164      setTimeout(function () {
3165        $("#newsletter").prop("checked", data.newsletter === "on" || data.newsletter === "1" || data.newsletter === 1);
3166        $("#newsletter_swing").prop("checked", data.newsletter_swing === "on" || data.newsletter_swing === "1" || data.newsletter_swing === 1);
3167      }, 300);
3168
3169      if (data.birthday) {
3170        $("#birthday").prop("disabled", true);
3171      }
3172    });
3173  }
3174
3175  function initAccountValidation() {
3176    if (typeof $.fn.validate === "undefined") return;
3177    var $forms = $("#account_registration, #account_update");
3178    if (!$forms.length) return;
3179    cv2EnsurePhoneValidator();
3180
3181    $forms.validate({
3182      ignore: ":not(:visible)",
3183      focusInvalid: false,
3184      rules: {
3185        nome: { required: true },
3186        email: { required: true, email: true },
3187        email2: { required: true, equalTo: "#email" },
3188        password: { required: true, minlength: 6 },
3189        password2: { equalTo: "#password" },
3190        morada: { required: true },
3191        localidade: { required: true },
3192        codigo_postal: { required: true },
3193        telefone: { required: true, cv2phone: true },
3194        sexo: { required: true }
3195        /* t1409: birthday opcional na conta */
3196      },
3197      invalidHandler: function (evt, validator) {
3198        /* t1589: sem popup — só highlight + scroll/foco no 1.º campo inválido */
3199        if (validator && validator.errorList && validator.errorList.length) {
3200          var el = validator.errorList[0].element;
3201          $("html, body").animate({ scrollTop: $(el).offset().top - 110 }, 1000);
3202          $(el).focus();
3203        }
3204      },
3205      onkeyup: function (element) {
3206        $(element).removeClass("LV_invalid_input");
3207      },
3208      onfocusout: function (element) {
3209        var $el = $(element);
3210        if ($el.valid && $el.valid()) {
3211          $el.removeClass("LV_invalid_input");
3212        } else {
3213          $el.addClass("LV_invalid_input");
3214        }
3215      },
3216      messages: (window.cv2Jqv && cv2Jqv.fieldMessages) ? cv2Jqv.fieldMessages() : {},
3217      errorPlacement: function (error, element) {
3218        if (window.cv2Jqv && cv2Jqv.errorPlacement) return cv2Jqv.errorPlacement(error, element);
3219        error.insertAfter(element);
3220      },
3221      showErrors: function (errorMap, errorList) {
3222        if (window.cv2Jqv && cv2Jqv.showErrorsWithLabels) {
3223          return cv2Jqv.showErrorsWithLabels.call(this, errorMap, errorList);
3224        }
3225        $.each(errorList, function (_, item) {
3226          $(item.element).addClass("LV_invalid_input");
3227        });
3228        this.defaultShowErrors();
3229      }
3230    });
3231  }
3232
3233  function getAccountFormPayload() {
3234    return {
3235      email: $("#email").val(),
3236      password: $("#password").val(),
3237      nome: $("#nome").val(),
3238      morada: $("#morada").val(),
3239      fiscal_id: $("#fiscal_id").val(),
3240      localidade: $("#localidade").val(),
3241      codigo_postal: $("#codigo_postal").val(),
3242      country: $("#country").val(),
3243      telefone: $("#telefone").val(),
3244      sexo: $("#sexo").val(),
3245      birthday: $("#birthday").val(),
3246      newsletter: $("#newsletter").is(":checked") ? "1" : "0",
3247      newsletter_swing: $("#newsletter_swing").is(":checked") ? "1" : "0",
3248      "g-recaptcha-response": $("#g-recaptcha-response").val()
3249    };
3250  }
3251
3252  function resetSubmitButton($submitButton) {
3253    if ($submitButton && $submitButton.length) {
3254      $submitButton.prop("disabled", false)
3255                   .html($submitButton.data("original-text") || $submitButton.text());
3256    }
3257  }
3258
3259  function submitAccountForm(action, $submitButton) {
3260    if (typeof grecaptcha !== "undefined" && !grecaptcha.getResponse()) {
3261      if (typeof swal === "function") {
3262        swal({
3263          title: gl("swal_attention") || "Atenção",
3264          text: gl("RECAPTCHAERR") || "Confirme o reCAPTCHA.",
3265          type: "warning",
3266          confirmButtonColor: "#ABD559",
3267          showCancelButton: false,
3268          animation: false,
3269          confirmButtonText: gl("swal_btnok") || "OK",
3270          closeOnConfirm: true
3271        });
3272      }
3273      resetSubmitButton($submitButton);
3274      return;
3275    }
3276
3277    var payload = getAccountFormPayload();
3278    var lang = $("#session_lang").val() || "pt";
3279
3280    $.ajax({
3281      type: "POST",
3282      url: REQUEST_ENDPOINT,
3283      cache: false,
3284      data: { ajax: action, formData: payload }
3285    }).done(function (res) {
3286      switch (String(res)) {
3287        case "0":
3288          swal({
3289            title: gl("swal_attention") || "Atenção",
3290            text: gl("NEWUSERERR") || "Ocorreu um erro ao criar a conta.",
3291            type: "error",
3292            confirmButtonColor: "#ABD559",
3293            showCancelButton: false,
3294            animation: false,
3295            confirmButtonText: gl("swal_btnok") || "OK",
3296            closeOnConfirm: true
3297          }, function () { window.location.href = "/" + lang + "/"; });
3298          break;
3299        case "1":
3300          swal({
3301            title: gl("swal_info") || "Informação",
3302            text: gl("NEWUSEROK") || "Conta criada com sucesso.",
3303            type: "success",
3304            confirmButtonColor: "#ABD559",
3305            showCancelButton: false,
3306            animation: false,
3307            confirmButtonText: gl("swal_btnok") || "OK",
3308            closeOnConfirm: true
3309          }, function () { window.location.href = "/" + lang + "/"; });
3310          break;
3311        case "3":
3312          swal({
3313            title: gl("swal_info") || "Informação",
3314            text: gl("NEWUSERDUP") || "Este e-mail já está registado.",
3315            type: "warning",
3316            confirmButtonColor: "#ABD559",
3317            showCancelButton: false,
3318            animation: false,
3319            confirmButtonText: gl("swal_btnok") || "OK",
3320            closeOnConfirm: true
3321          }, function () { window.location.href = "/" + lang + "/"; });
3322          break;
3323        case "mail-err":
3324          swal({
3325            title: gl("swal_attention") || "Atenção",
3326            text: gl("EMAILNOK") || "O e-mail que introduziu parece não existir, por favor valide o campo E-mail.",
3327            type: "warning",
3328            confirmButtonColor: "#ABD559",
3329            showCancelButton: fal
3329se,
3330            animation: false,
3331            confirmButtonText: gl("swal_btnok") || "OK",
3332            closeOnConfirm: true
3333          });
3334          break;
3335        case "4":
3336          swal({
3337            title: gl("swal_info") || "Informação",
3338            text: gl("UPDATEUSEROK") || "Dados atualizados com sucesso.",
3339            type: "success",
3340            confirmButtonColor: "#ABD559",
3341            showCancelButton: false,
3342            animation: false,
3343            confirmButtonText: gl("swal_btnok") || "OK",
3344            closeOnConfirm: true
3345          }, function () { window.location.href = "/" + lang + "/"; });
3346          break;
3347        case "recaptcha-err":
3348          swal({
3349            title: gl("swal_attention") || "Atenção",
3350            text: gl("RECAPTCHAHACK") || "Confirmação reCAPTCHA inválida.",
3351            type: "warning",
3352            confirmButtonColor: "#ABD559",
3353            showCancelButton: false,
3354            animation: false,
3355            confirmButtonText: gl("swal_btnok") || "OK",
3356            closeOnConfirm: true
3357          }, function () { window.location.href = "/" + lang + "/account/"; });
3358          break;
3359        default:
3360          toastr.info(gl("UPDATEUSEROK") || "Operação concluída.");
3361          break;
3362      }
3363    }).always(function () {
3364      resetSubmitButton($submitButton);
3365    });
3366  }
3367
3368  function initOrdersTable() {
3369    if (!$.fn.DataTable) return;
3370    var $table = $("#orders_table");
3371    if (!$table.length || $table.hasClass("dt-initialized")) return;
3372
3373    $table.on("xhr.dt", function () { /* placeholder for potential hooks */ });
3374
3375    $table.DataTable({
3376      responsive: true,
3377      processing: true,
3378      serverSide: true,
3379      searching: true,
3380      dom: "flrBtip",
3381      lengthMenu: [[10, 25, 100, -1], [10, 25, 100, "All"]],
3382      pageLength: 10,
3383      ajax: "/customer-dt-orders.php"
3384    });
3385
3386    if (typeof $.fn.dataTable !== "undefined" && $.fn.dataTable.ext && $.fn.dataTable.ext.search) {
3387      // Retain legacy delay helper if available
3388      if ($.fn.dataTable && $.fn.dataTableExt && $.fn.dataTableExt.oApi && $.fn.dataTableExt.oApi.fnSetFilteringDelay) {
3389        $table.dataTable().fnSetFilteringDelay(600);
3390      }
3391    }
3392
3393    $table.addClass("dt-initialized");
3394  }
3395
3396  function renderAccountLogin() {
3397    return $.ajax({
3398      type: "POST",
3399      url: REQUEST_ENDPOINT,
3400      cache: false,
3401      data: { ajax: "show_login" }
3402    }).done(function (html) {
3403      var $login = $("#account_login");
3404      if (!$login.length) return;
3405      $login.html(html).show();
3406      var $form = $("#login_cupidovip");
3407      if ($form.length) {
3408        $form.off("submit.accountLogin").on("submit.accountLogin", function (evt) {
3409          evt.preventDefault();
3410          processLogin();
3411        });
3412      }
3413    });
3414  }
3415
3416  function renderAccountForm() {
3417    return $.ajax({
3418      type: "POST",
3419      url: REQUEST_ENDPOINT,
3420      cache: false,
3421      data: { ajax: "show_register_form" }
3422    }).done(function (html) {
3423      var $wrap = $("#account_table_form");
3424      if (!$wrap.length) return;
3425
3426      $wrap.hide().empty().html(html).fadeIn(200);
3427
3428      var $reg = $("#account_registration");
3429      if ($reg.length) {
3430        $reg.off("submit.accountRegister").on("submit.accountRegister", function (evt) {
3431          evt.preventDefault();
3432          var $btn = $("#account_registration_submit");
3433          if ($reg.valid && !$reg.valid()) {
3434            resetSubmitButton($btn);
3435            return;
3436          }
3437          if ($btn.length) {
3438            $btn.data("original-text", $btn.html()).html('<i class="fa fa-cog fa-spin" style="color:inherit;"></i>').prop("disabled", true);
3439          }
3440          submitAccountForm("registerUser", $btn);
3441        });
3442      }
3443
3444      var $update = $("#account_update");
3445      if ($update.length) {
3446        $update.off("submit.accountUpdate").on("submit.accountUpdate", function (evt) {
3447          evt.preventDefault();
3448          var $btn = $("#account_update_submit");
3449          if ($update.valid && !$update.valid()) {
3450            resetSubmitButton($btn);
3451            return;
3452          }
3453          if ($btn.length) {
3454            $btn.data("original-text", $btn.html()).html('<i class="fa fa-cog fa-spin" style="color:inherit;"></i>').prop("disabled", true);
3455          }
3456          submitAccountForm("updateUser", $btn);
3457        });
3458      }
3459
3460      $(document)
3461        .off("click.accountRegistrationSubmit")
3462        .on("click.accountRegistrationSubmit", "#account_registration_submit", function () {
3463          $("#account_registration").trigger("submit");
3464        });
3465
3466      $(document)
3467        .off("click.accountUpdateSubmit")
3468        .on("click.accountUpdateSubmit", "#account_update_submit", function () {
3469          $("#account_update").trigger("submit");
3470        });
3471
3472      initAccountValidation();
3473      populateAccountForm();
3474      ensureAccountRecaptcha();
3475      initOrdersTable();
3476      /* t1623: reiniciar Select2 país após HTML AJAX da área de conta */
3477      try {
3478        if (typeof window.cv2CountrySelect2Init === "function") {
3479          window.cv2CountrySelect2Init();
3480        }
3481      } catch (eC) {}
3482      try {
3483        $("#account_table_form, #account_table_form .user_area_div, #account_update, #account_registration")
3484          .css("overflow", "visible");
3485      } catch (eO) {}
3486    });
3487  }
3488
3489  var accountDelegatesBound = false;
3490  function bindAccountDelegates() {
3491    if (accountDelegatesBound) return;
3492    accountDelegatesBound = true;
3493
3494    $(document)
3495      .off("click.accountRecover")
3496      .on("click.accountRecover", "#cupidovip_password_recovery", function () {
3497        if (typeof swal !== "function") return;
3498        swal({
3499          title: gl("swal_recoverpass_title") || "Recuperar password",
3500          text: gl("swal_recoverpass_message") || "Insira o seu e-mail.",
3501          type: "input",
3502          showCancelButton: true,
3503          closeOnConfirm: false,
3504          cancelButtonText: gl("swal_btncancel") || "Cancelar",
3505          confirmButtonText: gl("swal_btnok") || "OK",
3506          animation: false,
3507          showLoaderOnConfirm: true,
3508          inputPlaceholder: "E-mail"
3509        }, function (value) {
3510          if (value === false) return false;
3511          if (!value) {
3512            swal.showInputError(gl("swal_recoverpass_alert") || "Introduza um e-mail válido.");
3513            return false;
3514          }
3515
3516          if (typeof checkifemail === "function" && checkifemail(value) === false) {
3517            swal.showInputError(gl("swal_invalid_email") || "E-mail inválido.");
3518            return false;
3519          }
3520
3521          $.ajax({
3522            type: "POST",
3523            url: REQUEST_ENDPOINT,
3524            cache: false,
3525            data: { ajax: "shopcart_recover", email: value }
3526          }).done(function (res) {
3527            /* t1623: anti-enumeração — nunca revelar se o email existe */
3528            if (res === "ERROR") {
3529              swal.showInputError(gl("swal_email_blocked") || "E-mail bloqueado.");
3530            } else {
3531              var genMsg = gl("CV2_OTP_GENERIC_SENT") || "Receberá um email se o mesmo estiver registado.";
3532              swal(gl("swal_email_recovertitle") || "Pedido recebido", genMsg, "success");
3533            }
3534          });
3535        });
3536      });
3537
3538    $(document)
3539      .off("focusout.accountEmail")
3540      .on("focusout.accountEmail", "#account_registration #email", function () {
3541        var email = $(this).val();
3542        if (!email) return;
3543        $.ajax({
3544          type: "POST",
3545          url: REQUEST_ENDPOINT,
3546          cache: false,
3547          data: { ajax: "checkEmailWebstore", email: email }
3548        }).done(function (res) {
3549          if (parseInt(res, 10) === 1 && typeof swal === "function") {
3550            swal({
3551              title: gl("swal_info_cupidovip_alert") || "Já existe conta",
3552              text: gl("swal_info_cupidovip_message") || "Este e-mail já está registado.",
3553              type: "info",
3554              confirmButtonColor: "#ABD559",
3555              showCancelButton: false,
3556              animation: false,
3557              confirmButtonText: gl("swal_btnok") || "OK",
3558              closeOnConfirm: true
3559            }, function () {
3560              $("#usercesto").val(email);
3561              $("#email, #email2").val("").removeClass("LV_invalid_input");
3562              $("#usercesto").removeClass("LV_invalid_input");
3563              $("#passcesto").focus().removeClass("LV_invalid_input");
3564            });
3565          }
3566        });
3567      });
3568
3569    /* t1605: checkout — aviso BO-i18n, sem limpar o email (convidado permitido) */
3570    $(document)
3571      .off("focusout.checkoutEmailExists click.checkoutEmailExists")
3572      .on("focusout.checkoutEmailExists", "#encomenda #email", function () {
3573        var email = $.trim($(this).val() || "");
3574        var $hint = $("#cv2_email_exists_hint");
3575        if (!$hint.length) return;
3576        if (!email || email.indexOf("@") < 1) {
3577          $hint.prop("hidden", true);
3578          return;
3579        }
3580        $.ajax({
3581          type: "POST",
3582          url: REQUEST_ENDPOINT,
3583          cache: false,
3584          data: { ajax: "checkEmailWebstore", email: email }
3585        }).done(function (res) {
3586          if (parseInt(res, 10) === 1) {
3587            $hint.prop("hidden", false);
3588            if ($("#cv2_create_account").length && $("#cv2_create_account").is(":checked")) {
3589              $("#cv2_create_account").prop("checked", false).trigger("change");
3590            }
3591          } else {
3592            $hint.prop("hidden", true);
3593          }
3594        });
3595      })
3596      .on("click.checkoutEmailExists", "#cv2_email_exists_login", function (e) {
3597        e.preventDefault();
3598        var email = $.trim($("#encomenda #email").val() || "");
3599        if (email) {
3600          $("#usercesto, #cv2_otp_email").val(email);
3601        }
3602        $("#cv2_open_login").trigger("click");
3603      })
3604      .on("click.checkoutEmailExists", "#cv2_email_exists_dismiss", function (e) {
3605        e.preventDefault();
3606        $("#cv2_email_exists_hint").prop("hidden", true);
3607      });
3608  }
3609
3610  // o link "Recuperar acesso" tambem existe no painel de login do checkout —
3611  // os delegados sao globais (document) e idempotentes, ligar sempre
3612  bindAccountDelegates();
3613
3614  function initAccountSection() {
3615    if (!$("#account_html").length) return;
3616    bindAccountDelegates();
3617
3618    $.ajax({
3619      type: "POST",
3620      url: REQUEST_ENDPOINT,
3621      cache: false,
3622      data: { ajax: "checkUid" }
3623    }).done(function (res) {
3624      var isLogged = parseInt(res, 10) !== 0;
3625      if (!isLogged) {
3626        renderAccountLogin();
3627      }
3628      renderAccountForm();
3629    });
3630  }
3631
3632  window.handleCredentialResponse = function (response) {
3633    // CV3 (2026-08-07): o id_token do Google e validado no proprio site
3634    // (google-oauth.php valida com a Google e resolve o utilizador). Deixa de
3635    // haver o hop ao backend de producao nem um uid cru vindo do browser.
3636    $.post("/google-oauth.php", { credential: response.credential }, function (authRes) {
3637      var auth;
3638      try { auth = typeof authRes === "object" ? authRes : JSON.parse(authRes); }
3639      catch (e) { swal("Erro", "Resposta inválida do servidor.", "error"); return; }
3640      if (auth && auth.success) window.location.reload();
3641      else swal("Erro", "Falha ao validar o login Google.", "error");
3642    });
3643  };
3644
3645  function processLogin() {
3646    $.ajax({
3647      type: "POST", cache: false, url: REQUEST_ENDPOINT,
3648      data: { ajax: "makeLogin", usercesto: $("#usercesto").val(), passcesto: $("#passcesto").val(), csrf: window.cv2Csrf || "" },
3649      success: function (e) {
3650        var t = JSON.parse(e);
3651        if (t.csrf) window.cv2Csrf = t.csrf;
3652        if (t.failed) {
3653          swal({
3654            title: gl("swal_attention"),
3655            text: gl("swal_login_failed"),
3656            type: "info", confirmButtonColor: "#ABD559",
3657            showCancelButton: false, animation: false,
3658            confirmButtonText: gl("swal_btnok"), closeOnConfirm: true
3659          }, function (confirm) {
3660            if (confirm) { $("#passcesto").val(""); $("#usercesto").removeClass("LV_invalid_input"); $("#passcesto").removeClass("LV_invalid_input"); $("#usercesto").focus(); }
3661          });
3662          return;
3663        }
3664        $("#uid").val(t.id);
3665        generate_cupidovip(t.id);
3666        toastr.info(gl("CUPIDOVIP_LOGIN"));
3667        if ($("#shopcart_html").length) { populate_shopcart(); shopcart_specialservices(); }
3668        if ($("#account_html").length) { var lang = $("#session_lang").val(); window.location.href = "/" + lang + "/account/"; }
3669      }
3670    });
3671  }
3672
3673  /* ==========================================================
3674   * 9) ENTREGA / MAPAS / SHOPCART
3675   * ========================================================== */
3676  function clickPayMethod(id) {
3677    $("#paymethods_sibs, #paymethods_sibs_mb, #paymethods_sibs_mbway, #paymethods_paypal_image, #western_image").addClass("desaturate");
3678    switch (id) {
3679      case 'sc_payment_sibs_card': $("#paymethods_sibs").removeClass("desaturate"); break;
3680      case 'sc_payment_sibs_mb': $("#paymethods_sibs_mb").removeClass("desaturate"); break;
3681      case 'sc_payment_sibs_mbway': $("#paymethods_sibs_mbway").removeClass("desaturate"); break;
3682      case 'sc_payment_api_paypal': $("#paymethods_paypal_image").removeClass("desaturate"); break;
3683      case 'sc_payment_western': $("#western_image").removeClass("desaturate"); break;
3684    }
3685    $(".paymethod").removeClass("shopcart_paytype_button_selected");
3686    $("#" + id).addClass("shopcart_paytype_button_selected");
3687  }
3688
3689  function clickDeliveryMethod(e) {
3690    switch (e) {
3691      case "sc_delivery_home":
3692        $("#ctt_image,#worten_image").addClass("desaturate");
3693        $("#selected_deliverymethod_aux").val("sc_delivery_home");
3694        $("#delivery_method_api,#delivery_api_output,#shop_cart_table_delivery").slideUp();
3695        $("#sc_payment_reception").show();
3696        break;
3697      case "sc_delivery_alternative":
3698        $("#ctt_image,#worten_image").addClass("desaturate");
3699        $("#selected_deliverymethod_aux").val("sc_delivery_alternative");
3700        $("#delivery_method_api,#delivery_api_output").slideUp();
3701        $("#shop_cart_table_delivery").fadeIn(700);
3702        $("html, body").animate({ scrollTop: $("#shop_cart_table_delivery").position().top - 110 }, "slow");
3703        $("#delivery_nome").focus();
3704        $("#sc_payment_reception").show();
3705        break;
3706      case "sc_delivery_ctt":
3707        $("#ctt_image").removeClass("desaturate"); $("#worten_image").addClass("desaturate");
3708        if (!/^(sc_delivery_ctt|sc_delivery_worten)$/.test($("#selected_deliverymethod_aux").val())) $("#delivery_method_api").slideUp();
3709        $("#selected_deliverymethod_aux").val("sc_delivery_ctt");
3710        $("#delivery_api_output").hide(); $("#shop_cart_table_delivery").slideUp();
3711        callAPIresults(2); $("#sc_payment_reception").hide(); break;
3712      case "sc_delivery_worten":
3713        $("#worten_image").removeClass("desaturate"); $("#ctt_image").addClass("desaturate");
3714        if (!/^(sc_delivery_ctt|sc_delivery_worten)$/.test($("#selected_deliverymethod_aux").val())) $("#delivery_method_api").slide
3714Up();
3715        $("#selected_deliverymethod_aux").val("sc_delivery_worten");
3716        $("#delivery_api_output").hide(); $("#shop_cart_table_delivery").slideUp();
3717        callAPIresults(1); $("#sc_payment_reception").hide(); break;
3718    }
3719    $(".deliverymethod").removeClass("shopcart_delivery_button_selected");
3720    $("#" + e).addClass("shopcart_delivery_button_selected");
3721    // o metodo de entrega muda a tabela de portes (ex.: ponto CTT) — refrescar resumo
3722    if (typeof populate_shopcart === "function") populate_shopcart();
3723  }
3724
3725  function callAPIresults(e, t) {
3726    var langctt = $("#session_lang").val();
3727    if (e == "pt") { langctt = "pt"; e = 1; }
3728    if (e == "es") { langctt = "es"; e = 1; }
3729
3730    if (!$("#shopcart-api").length) return;
3731
3732    // Pesquisa server-side (getCTT_search): o select2 pede so os resultados do termo.
3733    // O fluxo antigo carregava todas as options (api_ctt_es >22k = 1.4MB) e congelava o browser.
3734    var $points = $("#api_points");
3735    if ($points.data("select2")) $points.select2("destroy");
3736    $points.empty().prop("disabled", false);
3737
3738    // Mudar de pais faz reset a escolha anterior (ponto + painel de detalhes)
3739    $("#delivery_api_output").hide();
3740    $("#api-selected-html").empty();
3741    $("#api-selected-address").empty();
3742    $("#shopcart-api-txt").empty();
3743
3744    $points.select2({
3745      minimumInputLength: 2,
3746      language: {
3747        inputTooShort: function () { return gl("CV2_CTT_TYPE2") || "Escreva pelo menos 2 letras"; },
3748        searching: function () { return gl("CV2_CTT_SEARCHING") || "A procurar..."; },
3749        noResults: function () { return gl("CV2_CTT_NORESULTS") || "Sem resultados"; }
3750      },
3751      ajax: {
3752        url: REQUEST_ENDPOINT,
3753        type: "POST",
3754        dataType: "json",
3755        delay: 250,
3756        data: function (params) {
3757          return { ajax: "getCTT_search", lang: langctt, term: params.term };
3758        },
3759        processResults: function (data) { return data && data.results ? data : { results: [] }; },
3760        cache: true
3761      }
3762    });
3763
3764    // Ao escolher um ponto, mostrar logo os detalhes (binding em falta no fluxo antigo)
3765    $(document).off("change.cttPoint", "#api_points").on("change.cttPoint", "#api_points", function () {
3766      var v = $(this).val();
3767      if (v) triggerAPIcall(v);
3768    });
3769
3770    $("#delivery_method_api").fadeIn(700);
3771    if (jQuery.browser && jQuery.browser.mobile) $("html, body").animate({ scrollTop: $("#delivery_method_api").position().top - 120 }, "slow");
3772
3773    if (t) {
3774      $points.append(new Option("CTT", t, true, true)).trigger("change");
3775    } else {
3776      $points.select2("open");
3777      $(".select2-search__field").trigger("click").focus();
3778    }
3779  }
3780  window.callAPIresults = callAPIresults;
3781
3782  // \b\w quebrava em acentos ("Algés" -> "AlgéS"); so capitaliza no inicio de palavra real
3783  function ucword(str) { return str.toLowerCase().replace(/(^|[\s(\/\-])[a-zà-öø-ÿ]/g, function (l) { return l.toUpperCase(); }); }
3784
3785  function triggerAPIcall(selectedPaymentMethod) {
3786    $("#selected_paymethod_aux_select").val(selectedPaymentMethod);
3787    var lang_ctt = (document.getElementById('ctt_lang_es') && document.getElementById('ctt_lang_es').checked) ? "es" : "pt";
3788
3789    if (!selectedPaymentMethod) return;
3790
3791    $.ajax({
3792      type: "POST", async: true, cache: false, url: REQUEST_ENDPOINT, dataType: "json",
3793      data: { ajax: "getAPIdetail", lang: lang_ctt, ctt_id: selectedPaymentMethod },
3794      success: function (response) {
3795        var latitude = response.latitude, longitude = response.longitude, town = response.town,
3796          pointName = response.point_name, streetName = response.street_name, postalcode = response.postalcode,
3797          schedule = response.schedule, phone = response.phone, tipo = response.tipo;
3798
3799        var formattedStreetName = ucword((streetName || "").toLowerCase());
3800        var formattedPointName  = ucword((pointName  || "").toLowerCase());
3801        var formattedSchedule   = schedule || "";
3802        var cttCountryLabel = (lang_ctt === "es") ? "España" : "Portugal";
3803        var cttLocalidade = postalcode + " " + town;
3804        if (String(town).toLowerCase().indexOf(cttCountryLabel.toLowerCase()) === -1) cttLocalidade += ", " + cttCountryLabel;
3805        var cttPhone = String(phone || "").replace(/^\s*tel\.?:?\s*/i, "");
3806
3807        var metaBits = [];
3808        if (formattedSchedule) {
3809          metaBits.push("<span class='cv2-ctt-metaitem'><svg viewBox='0 0 24 24' width='15' height='15' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' aria-hidden='true'><circle cx='12' cy='12' r='9'/><path d='M12 7v5l3 2'/></svg>" + formattedSchedule + "</span>");
3810        }
3811        metaBits.push("<span class='cv2-ctt-metaitem'><svg viewBox='0 0 24 24' width='15' height='15' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' aria-hidden='true'><path d='M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z'/><circle cx='12' cy='10' r='3'/></svg>" + cttLocalidade + "</span>");
3812        if (cttPhone) {
3813          metaBits.push("<span class='cv2-ctt-metaitem'><svg viewBox='0 0 24 24' width='15' height='15' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' aria-hidden='true'><path d='M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3 19.5 19.5 0 0 1-6-6 19.8 19.8 0 0 1-3-8.7A2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.13.96.36 1.9.7 2.8a2 2 0 0 1-.45 2.1L8.1 9.9a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.45c.9.34 1.84.57 2.8.7A2 2 0 0 1 22 16.9Z'/></svg>" + cttPhone + "</span>");
3814        }
3815
3816        $("#api-selected-html").html(
3817          "<h3 class='cv2-ctt-name'>" + formattedPointName + "</h3>" +
3818          "<div class='cv2-ctt-addr'>
3818" + formattedStreetName + "<br>" + cttLocalidade + "</div>" +
3819          "<div class='cv2-ctt-meta'>" + metaBits.join("<span class='cv2-ctt-metasep'></span>") + "</div>"
3820        );
3821        // CTT tem as caixas de info/OBS estaticas no novo layout; so o worten injeta texto aqui
3822        $("#shopcart-api-txt").html(String(tipo) === "2" ? "" : "<br><span class='api_txt'>" + gl("info_worten") + "</span>"); // tipo chega como string
3823        $("#delivery_api_output").show();
3824
3825        // Restore: substituir o texto provisorio da option pelo nome real do ponto
3826        var $cttOpt = $("#api_points option[value='" + selectedPaymentMethod + "']");
3827        if ($cttOpt.length && $cttOpt.text() === "CTT") { $cttOpt.text(formattedPointName); $("#api_points").trigger("change.select2"); }
3828
3829        if (latitude || longitude) {
3830          $("#api-selected-address").show();
3831          initMap(latitude, longitude, formattedPointName, selectedPaymentMethod);
3832        } else {
3833          initMapByAddress(formattedStreetName, town, formattedPointName);
3834        }
3835
3836        check_shopcart();
3837        // portes dependem da zona do ponto CTT — refrescar o resumo (aside/sidebar)
3838        if (typeof populate_shopcart === "function") populate_shopcart();
3839        $("#api_points_container").removeClass("LV_invalid_input");
3840      }
3841    });
3842  }
3843
3844  function initMapByAddress(address, city, title) {
3845    if (!address || !city) return;
3846    var fullAddress = address + ", " + city;
3847    var geocoder = new google.maps.Geocoder();
3848    geocoder.geocode({ address: fullAddress }, function (results, status) {
3849      if (status !== "OK") { console.error("Geocode failed:", status); return; }
3850      var loc = results[0].geometry.location;
3851      var myLatLng = { lat: loc.lat(), lng: loc.lng() };
3852      var map = new google.maps.Map(document.getElementById("api-selected-address"), { center: myLatLng, scrollwheel: false, zoom: 16 });
3853      var marker = new google.maps.Marker({
3854        map: map, position: myLatLng, title: title,
3855        url: "https://www.google.com/maps/dir/Current+Location/" + myLatLng.lat + "," + myLatLng.lng
3856      });
3857      google.maps.event.addListener(marker, "click", function () { window.open(this.url, "_blank"); });
3858    });
3859  }
3860
3861  function initMap(e, t, n) {
3862    if (!t) return;
3863    var myLatLng = { lat: parseFloat(e), lng: parseFloat(t) };
3864    var map = new google.maps.Map(document.getElementById("api-selected-address"), { center: myLatLng, scrollwheel: false, zoom: 16 });
3865    var marker = new google.maps.Marker({
3866      map: map, position: myLatLng, title: n,
3867      url: "https://www.google.com/maps/dir/Current+Location/" + e + "," + t
3868    });
3869    google.maps.event.addListener(marker, "click", function () { window.open(this.url, "_blank"); });
3870    // o container acabou de ser mostrado — sem resize o mapa fica cinzento (medido a 0x0)
3871    window.setTimeout(function () { google.maps.event.trigger(map, "resize"); map.setCenter(myLatLng); }, 400);
3872  }
3873
3874  function checkSelectedDeliveryMethod() {
3875    var e = 0; $(".deliverymethod").each(function () { if ($(this).hasClass("shopcart_delivery_button_selected")) e = $(this).attr("id"); }); return e;
3876  }
3877  function checkSelectedPaymentMethod() {
3878    var e = 0; $(".paymethod").each(function () { if ($(this).hasClass("shopcart_paytype_button_selected")) e = $(this).attr("id"); }); return e;
3879  }
3880
3881  // ===== gerar cesto completo (página /cart/) =====
3882  function generate_shopcart(type, discountOverride) {
3883    var discount_code = typeof discountOverride !== "undefined"
3884      ? discountOverride
3885      : (($(".checkout-modern__cart-aside #discount_code").first().val() || $("#discount_code").last().val()) || "");
3886    var t = type || "html";
3887
3888    $.ajax({
3889      type: "POST",
3890      url: REQUEST_ENDPOINT,
3891      cache: false,
3892      async: false, // manter compat com o antigo
3893      data: {
3894        ajax: "generate_shopcart",
3895        type: t,
3896        discount_code: discount_code
3897      },
3898      success: function (html) {
3899        // injetar HTML
3900        $("#shopcart_html").html(html);
3901
3902        // lazy das imagens do cesto
3903        if ($.fn.lazy) {
3904          $(".lazy").lazy({
3905            bind: "event",
3906            effect: "fadeIn",
3907            effectTime: 500,
3908            threshold: 0
3909          });
3910        }
3911
3912        // se havia código, validar quantidades
3913        if (discount_code) {
3914          if (typeof validate_discount_code_qtd === "function") {
3915            validate_discount_code_qtd(discount_code);
3916          }
3917        }
3918
3919        // blocos especiais do teu legado
3920        if ($("#google_first").val() === "1") {
3921          $("#shopcart_option_buttons").hide();
3922          $("#shopcart_login").show();
3923          $("#delivery_methods").fadeIn(500);
3924          $("#shop_cart_table_form").fadeIn(500);
3925          $("#payment_methods").fadeIn(500);
3926          $("#comments_table").fadeIn(500);
3927          $("#gift_option_table").fadeIn(500);
3928          $("#gift_message_table").fadeIn(500);
3929          $("#optionals_table").fadeIn(500);
3930          $("#shop_cart_footer").fadeIn(500);
3931          $("#shop_cart_footer_info").fadeIn(500);
3932          $("#shop_cart_footer_iva").fadeIn(500);
3933
3934          if (typeof generate_cupidovip === "function") {
3935            generate_cupidovip($("#uid").val());
3936          }
3937        }
3938
3939        // depois de gerar o cesto, aplica serviços especiais
3940        if (typeof shopcart_specialservices === "function") {
3941          shopcart_specialservices();
3942        }
3943
3944        if (typeof initFancybox === "function") {
3945          initFancybox();
3946        }
3947      }
3948    });
3949  }
3950
3951  // Checkout page: keep the summary/products/totals in sync with drawer
3952  // mutations. Same ajax the qty handler uses (generate_shopcart + populate).
3953  // Do NOT call check_shopcart() here: its empty path fades out the form.
3954  function refreshCheckoutAfterCartChange() {
3955    if (!$(".checkout-modern").length || !$("#shopcart_html").length) return;
3956    if ($(".checkout-modern.cv2-confirmed").length || $("#order_info").is(":visible")) return;
3957    if (window.__cv2Leaving) return;
3958    if (window.__cv2CheckoutSyncing) return;
3959    window.__cv2CheckoutSyncing = true;
3960    var reloadType = $("#delivery_methods").is(":visible") ? "shopcart_remove" : "html";
3961    if (typeof generate_shopcart === "function") generate_shopcart(reloadType);
3962    if (typeof populate_shopcart === "function") populate_shopcart();
3963    if (typeof window.refreshCheckoutProductsCard === "function") {
3964      window.refreshCheckoutProductsCard({ refreshLists: false });
3965    }
3966    window.setTimeout(function () { window.__cv2CheckoutSyncing = false; }, 800);
3967  }
3968
3969  // ===== gerar só o bloco de descontos do cesto =====
3970  function generate_shopcart_discounts(code) {
3971    $.ajax({
3972      type: "POST",
3973      url: REQUEST_ENDPOINT,
3974      cache: false,
3975      data: {
3976        ajax: "generate_shopcart_discounts",
3977        discount_code: code || ""
3978      },
3979      success: function (html) {
3980        // este ID tem de existir no HTML do cesto
3981        $("#shopcart_discount_html").html(html);
3982
3983        // se usas o Floatl (antigo) para o label flutuante
3984        if (typeof Floatl !== "undefined") {
3985          new Floatl($("#discount_code").parent());
3986        }
3987      }
3988    });
3989  }
3990
3991  var cv2PopulateSeq = 0;
3992  function populate_shopcart(context) {
3993    var ctx = context || "";
3994    // guard contra corridas: so a resposta do pedido MAIS RECENTE e aplicada
3995    // (mudar de pais disparava varios populates e a resposta antiga sobrescrevia os portes)
3996    var cv2Seq = ++cv2PopulateSeq;
3997    var userId = parseInt($("#uid").val(), 10) || 0; // "0" (string) e truthy — guests eram tratados como logados
3998    var selectedCountry = $("#country").val();
3999    var userCountry = $("#user_country").val();
4000    // t1644: ficha de envio manda; user_country (conta/sessao/encomenda anterior) so fallback
4001    if (!selectedCountry && userCountry) selectedCountry = userCountry;
4002    if (selectedCountry) $("#user_country").val(selectedCountry);
4003
4004    var deliveryCountry = $("#delivery_country").val();
4005    var selectedDeliveryMethod = checkSelectedDeliveryMethod();
4006    var selectedPaymentMethod  = checkSelectedPaymentMethod();
4007    var discountCode = ($(".checkout-modern__cart-aside #discount_code").first().val() || $("#discount_code").last().val()) || "";
4008    var specialService = $("#special_service").val();
4009    var sendAsGiftValue = $("#send_as_gift_value").val();
4010    var sendAsGift = $('input[name="send_as_gift"]').is(':checked') ? "on" : "";
4011    var api_points = (selectedDeliveryMethod == "sc_delivery_ctt" || selectedDeliveryMethod == "sc_delivery_worten") ? $("#api_points").val() : 0;
4012
4013    // garantir que o método CTT aparece (comportamento legado)
4014    $("#sc_delivery_ctt").parent().parent().show();
4015
4016    $.ajax({
4017      type: "POST", async: true, cache: false, url: REQUEST_ENDPOINT,
4018      data: {
4019        ajax: "populate_shopcart",
4020        uid: userId, country: selectedCountry, delivery_country: deliveryCountry,
4021        delivery_method: selectedDeliveryMethod, payment_method: selectedPaymentMethod,
4022        special_service: specialService, discount_code: discountCode,
4023        send_as_gift_value: sendAsGiftValue, send_as_gift: sendAsGift,
4024        // CV3 (2026-08-12): embrulhos multi — modo/linhas/dedicatória
4025        gift_wrap_enabled: $("#gift_wrap_enabled").val() || "0",
4026        gift_wrap_mode: $("#gift_wrap_mode").val() || "general",
4027        gift_wrap_lines: $("#gift_wrap_lines").val() || "",
4028        gift_wrap_dedicatoria: $("#gift_wrap_dedicatoria").val() || "",
4029        api_points: api_points
4030      },
4031      success: function (response) {
4032        if (!response) return;
4033        if (cv2Seq !== cv2PopulateSeq) return; // resposta obsoleta (chegou outra depois)
4034        var cartData = {};
4035        if (typeof response === "string") {
4036          try { cartData = JSON.parse(response); }
4037          catch (err) { cartData = {}; }
4038        } else if (typeof response === "object") {
4039          cartData = response || {};
4040        }
4041
4042        if ($(".checkout-modern__layout--cart-page").length) {
4043          var modernItemCount = parseInt(cartData.all_total_items || cartData.total_items || cartData.items || 0, 10);
4044          var modernHasItems = !isNaN(modernItemCount) && modernItemCount > 0;
4045          $("body").toggleClass("checkout-modern-cart-empty", !modernHasItems);
4046          if (!modernHasItems) {
4047            $(".checkout-modern__cart-aside #shop_cart_footer, .checkout-modern__cart-aside #shop_cart_footer_info, .checkout-modern__promo, .checkout-modern__cart-checkout").hide();
4048          } else {
4049            $(".checkout-modern__cart-aside").show();
4050            $(".checkout-modern__cart-aside #shop_cart_footer, .checkout-modern__cart-aside #shop_cart_footer_info, .checkout-modern__promo, .checkout-modern__cart-checkout").show();
4051          }
4052          var modernNoDiscount = cartData.subtotal_nodiscount_cambio || cartData.subtotal_nodiscount || "0.00";
4053          var modernDiscount = cartData.discount_cambio || cartData.discount || "0.00";
4054          var modernTotal = cartData.subtotal_cambio || cartData.subtotal || cartData.bigtotal_cambio || cartData.bigtotal || "0.00";
4055          var modernHasDiscount = modernDiscount && modernDiscount !== "0" && modernDiscount !== "0.00" && $.trim(discountCode || "") !== "";
4056          $(".checkout-modern-total-no-discount").text(modernNoDiscount);
4057          $(".checkout-modern-discount-value").text(modernDiscount);
4058          $(".checkout-modern-total-value").text(modernTotal);
4059          $(".checkout-modern__promo #apply_discount")
4060            .val(modernHasDiscount ? "REMOVER" : "APLICAR")
4061            .toggleClass("is-removing-discount", modernHasDiscount)
4062            .data("appliedCode", modernHasDiscount ? discountCode : "");
4063        }
4064
4065
4066        updateHeaderCart(cartData);
4067
4068        function toggleRow(id, on) {
4069          // cv3: o proprio <li> e o metodo (a lista nova nao tem wrappers .pure-*;
4070          // o parent().parent() antigo escondia a LISTA INTEIRA — USA mostrava PayPal)
4071          var $m = $("#" + id);
4072          var $el = $m.is("li") ? $m : $m.parent().parent();
4073          if (on) $el.stop(true, true).fadeIn(150);
4074          else $el.stop(true, true).hide();
4075        }
4076        function fallbackPayment() {
4077          // selecionado ficou indisponivel p/ este pais: passar ao 1o metodo visivel
4078          var $first = $("#payment_methods .paymethod:visible").first();
4079          if ($first.length && !$first.hasClass("shopcart_paytype_button_selected")) $first.trigger("click");
4080        }
4081
4082        toggleRow("sc_payment_reception", cartData.cobranca_status !== "off");
4083
4084        [["sc_payment_transfer","sc_estado3_status"],
4085         ["sc_payment_western","sc_estado4_status"],
4086         ["sc_payment_api_paypal","sc_estado5_status"],
4087         ["sc_payment_sibs_card","sc_estado6_status"],
4088         ["sc_payment_sibs_mb","sc_estado7_status"],
4089         ["sc_payment_sibs_mbway","sc_estado8_status"]].forEach(function (pair) {
4090          var id = pair[0], key = pair[1], isOn = cartData[key] !== "off";
4091          toggleRow(id, isOn);
4092        });
4093        // wallets acompanham o estado do paypal (mesmo carril)
4094        toggleRow("sc_payment_api_gpay", cartData.sc_estado5_status !== "off" && $("#sc_payment_api_gpay").hasClass("cv2-wallet-ready"));
4095        toggleRow("sc_payment_api_apple", cartData.sc_estado5_status !== "off" && $("#sc_payment_api_apple").hasClass("cv2-wallet-ready"));
4096        if ($("#" + selectedPaymentMethod).length && !$("#" + selectedPaymentMethod).is(":visible")) fallbackPayment();
4097        if (cartData.cobranca_status === "off" && selectedPaymentMethod === "sc_payment_reception") fallbackPayment();
4098
4099        selectedPaymentMethod = checkSelectedPaymentMethod();
4100
4101        var discountValue = cartData.discount_cambio || cartData.discount;
4102        var hasDiscount = discountValue && discountValue !== "0" && discountValue !== "0.00";
4103        var $discountBlocks = $("#discount_container_left, #discount_container_right");
4104        if (hasDiscount) {
4105          $discountBlocks.stop(true, true).fadeIn(500);
4106          $("#sc_discount").html(discountValue);
4107          if (cartData.discount_percent) {
4108            $("#sc_discount_percent").html("(" + cartData.discount_percent + "%)");
4109          }
4110        } else {
4111          $discountBlocks.stop(true, true).fadeOut(500);
4112          $("#sc_discount, #sc_discount_percent").empty();
4113        }
4114
4115        [
4116          ["#sc_subtotal", cartData.subtotal_cambio || cartData.subtotal],
4117          ["#sc_cobranca", cartData.cobranca_cambio || cartData.cobranca],
4118          ["#sc_shipping", cartData.shipping_cambio || cartData.shipping],
4119          ["#sc_weight", cartData.weight ? cartData.weight + " gr" : ""],
4120          ["#sc_total", cartData.subtotal_nodiscount_cambio || cartData.subtotal_nodiscount],
4121          ["#sc_bigtotal", cartData.bigtotal_cambio || cartData.bigtotal],
4122          ["#sc_optionals", cartData.sc_optionals],
4123          ["#sc_bigtotal_euros", cartData.bigtotal],
4124          ["#sc_iva", cartData.sc_iva],
4125          ["#sc_subtotal_comiva", cartData.sc_subtotal_comiva],
4126          ["#sc_iva_incidencia_23", cartData.sc_iva_incidencia_23],
4127          ["#sc_iva_total_23", cartData.sc_iva_total_23],
4128          ["#sc_iva_incidencia_6", cartData.sc_iva_incidencia_6],
4129          ["#sc_iva_total_6", cartData.sc_iva_total_6]
4130        ].forEach(function (entry) {
4131          if (typeof entry[1] !== "undefined" && entry[1] !== null) {
4132            $(entry[0]).html(entry[1]);
4133          }
4134        });
4135
4136        // CV3 (2026-08-12): embrulhos de presente — render a partir dos dados
4137        // autoritativos do servidor (preço/linhas/modo). checkout-modern.js expõe
4138        // cv3RenderGiftwrap; se não existir (outra página), ignora sem erro.
4139        if (cartData.giftwrap && typeof window.cv3RenderGiftwrap === "function") {
4140          window.cv3RenderGiftwrap(cartData.giftwrap);
4141        }
4142
4143        $("#sc_information_text").html(cartData.obs || "");
4144        if (cartData.obs) $("#sc_information_text_container").stop(true, true).fadeIn(200);
4145        else $("#sc_information_text_container").hide();
4146
4147        // t1644: nao repor pais da sessao por cima da ficha
4148        var _fc = $("#country").val();
4149        if (_fc) $("#user_country").val(_fc);
4150        else if (cartData.user_country) $("#user_country").val(cartData.user_country);
4151        if (typeof cartData.email_notes !== "undefined") $("#email_notes").val(cartData.email_notes);
4152
4153        var totalItems = parseInt(cartData.all_total_items || cartData.total_items || cartData.items || 0, 10);
4154        if (!isNaN(totalItems)) {
4155          var label = totalItems === 1 ? gl("ITEM") : gl("ITEMS");
4156          $(".basketId").text(totalItems + " " + label);
4157          $(".basketIdMobile").text(totalItems);
4158        }
4159
4160        if (cartData.subtotal_cambio) {
4161          var currency = ($("#session_currency").val() || "").trim();
4162          var subtotalText = currency ? currency + " " + cartData.subtotal_cambio : cartData.subtotal_cambio;
4163          $(".basketTotalId").text(subtotalText);
4164          $(".basketTotalIdMobile").text(subtotalText);
4165        }
4166
4167        if (ctx === "shopcart_initial_load" && typeof toastr !== "undefined" && Number(cartData.total_items || 0) >= 1) {
4168          if (Number(cartData.bigtotal_cambio || 0) <= Number(cartData.freeshipping_pt_cambio || 0)) {
4169            toastr.info(gl("CESTO_CONTINUAR"));
4170          }
4171          toastr.info(gl("TXT_TITLE_FINALIZAR_ENCOMENDA"));
4172        }
4173      }
4174    });
4175  }
4176
4177  // ===== Inicializar Fancybox em todas as imagens =====
4178  function initFancybox() {
4179  if (typeof $.fancybox === "undefined") return;
4180
4181  // links que são iframe
4182  var $iframeLinks = $(
4183    "a.fancybox\\.iframe, " +
4184    "a.fancybox[data-fancybox-type='iframe'], " +
4185    "a.fancybox[data-fb='iframe']"
4186  );
4187
4188  $iframeLinks.fancybox({
4189    type: 'iframe',
4190    smallBtn: true,
4191    iframe: {
4192      css: {
4193        width:  '500px',
4194        height: '450px',
4195        maxWidth: '100%',
4196        maxHeight: '100%'
4197      }
4198    }
4199  });
4200
4201  // resto das fancybox normais
4202  $(".fancybox")
4203    .not($iframeLinks)
4204    .fancybox();
4205}
4206
4207$(function () {
4208  initFancybox();
4209});
4210
4211
4212  /* ==========================================================
4213   * 9.1) ENCOMENDA (SUBMIT)
4214   * ========================================================== */
4215  function setSubmitLoading(isLoading) {
4216    var $btn = $("#shopcart_submit");
4217    if (!$btn.length) return;
4218    if (isLoading) {
4219      if (!$btn.data("orig-html")) {
4220        $btn.data("orig-html", $btn.html());
4221      }
4222      // estado visivel por tipo de metodo: pagamento imediato = "a iniciar o
4223      // pagamento"; assincrono (ref. MB/transferencia/cobranca) = "a registar"
4224      var cv2PayNow = { sc_payment_sibs_card: 1, sc_payment_sibs_mbway: 1, sc_payment_api_paypal: 1 };
4225      var cv2M = checkSelectedPaymentMethod();
4226      var cv2Txt = cv2PayNow[cv2M]
4227        ? (gl("CV2_CTA_STARTING") || "A iniciar o pagamento")
4228        : (gl("CV2_CTA_PLACING") || "A registar a encomenda");
4229      $btn.prop("disabled", true).attr("aria-busy", "true")
4230        .html('<i class="fa fa-cog fa-spin" style="color:inherit;"></i> <span class="cv2-cta-status">' + cv2Txt + '</span>');
4231    } else {
4232      var original = $btn.data("orig-html");
4233      if (original) $btn.html(original);
4234      $btn.prop("disabled", false).removeAttr("aria-busy");
4235    }
4236  }
4237
4238  function gatherShopcartRequestPayload() {
4239    var uid = parseInt($("#uid").val(), 10) || 0;
4240    var country = $("#country").val();
4241    var userCountry = $("#user_country").val();
4242        // t1644: ficha manda
4243        if (!country && userCountry) country = userCountry;
4244        if (country) $("#user_country").val(country);
4245
4246    var deliveryMethod = checkSelectedDeliveryMethod();
4247    var sendGift = $('input[name="send_as_gift"]').is(':checked');
4248
4249    return {
4250      ajax: "populate_shopcart",
4251      uid: uid,
4252      country: country,
4253      delivery_country: $("#delivery_country").val(),
4254      delivery_method: deliveryMethod,
4255      payment_method: checkSelectedPaymentMethod(),
4256      special_service: $("#special_service").val(),
4257      discount_code: $("#discount_code").val(),
4258      send_as_gift_value: $("#send_as_gift_value").val(),
4259      send_as_gift: sendGift ? "on" : "",
4260      // CV3 (2026-08-12): embrulhos multi (modo + linhas selecionadas + dedicatória)
4261      gift_wrap_enabled: $("#gift_wrap_enabled").val() || "0",
4262      gift_wrap_mode: $("#gift_wrap_mode").val() || "general",
4263      gift_wrap_lines: $("#gift_wrap_lines").val() || "",
4264      gift_wrap_dedicatoria: $("#gift_wrap_dedicatoria").val() || "",
4265      api_points: (deliveryMethod === "sc_delivery_ctt" || deliveryMethod === "sc_delivery_worten") ? $("#api_points").val() : 0
4266    };
4267  }
4268
4269  function gatherOrderFormData() {
4270    var data = {};
4271    data.email = $("#email").val() || $("#cacc_email").val();
4272    data.password = $("#password").val();
4273    data.nome = $("#nome").val() || $("#cacc_nome").val();
4274    data.fiscal_id = $("#fiscal_id").val() || $("#cacc_fiscal").val();
4275    data.morada = $("#morada").val() || $("#cacc_morada").val();
4276    data.localidade = $("#localidade").val() || $("#cacc_localidade").val();
4277    data.provincia = $("#provincia").val() || $("input[name='provincia']").val();
4278    data.codigo_postal = $("#codigo_postal").val() || $("#cacc_cp").val();
4279    data.country = $("#country").val() || $("#cacc_country").val();
4280    data.telefone = $("#telefone").val() || $("#cacc_telefone").val();
4281    data.sexo = $("#sexo").val();
4282    data.newsletter = $("#newsletter").val();
4283    data.birthday = $("#birthday").val();
4284    data.newsletter_swing = $("#newsletter_swing").val();
4285    data.delivery_nome = $("#delivery_nome").val();
4286    data.delivery_morada = $("#delivery_morada").val();
4287    data.delivery_localidade = $("#delivery_localidade").val();
4288    data.delivery_provincia = $("#delivery_provincia").val() || "";
4289    data.delivery_codigo_postal = $("#delivery_codigo_postal").val();
4290    data.delivery_country = $("#delivery_country").val();
4291    data.google_first = $("#google_first").val();
4292    data.cv2_create_account = $("#cv2_create_account").is(":checked") ? 1 : "";
4293    return data;
4294  }
4295
4296  // Idempotência (2026-08-15): chave imprevisível por TENTATIVA LÓGICA de
4297  // criação da encomenda. Mantém-se em retry de rede (o servidor faz replay da
4298  // resposta original — nunca cria segunda encomenda); renova-se apenas quando
4299  // o servidor devolve uma falha de negócio (a tentativa seguinte é nova).
4300  function cv2IdemKey(reset) {
4301    if (reset || !window.__cv2IdemKey) {
4302      var hex = "";
4303      try {
4304        var a = new Uint8Array(32);
4305        (window.crypto || window.msCrypto).getRandomValues(a);
4306        for (var i = 0; i < a.length; i++) hex += ("0" + a[i].toString(16)).slice(-2);
4307      } catch (e) {
4308        hex = "";
4309        for (var j = 0; j < 64; j++) hex += Math.floor(Math.random() * 16).toString(16);
4310      }
4311      window.__cv2IdemKey = hex;
4312    }
4313    return window.__cv2IdemKey;
4314  }
4315
4316  function gatherOrderData() {
4317    var data = {};
4318    data.idempotency_key = cv2IdemKey();
4319    // MBWAY: o telemóvel introduzido no checkout TEM de seguir para o servidor
4320    // (antes nunca era enviado e o backend usava o telefone da conta em bruto)
4321    data.mbway_phone = $.trim($("#sc_payment_sibs_mbway .checkout-modern__mbway-input").val() || "");
4322    data.delivery_method = checkSelectedDeliveryMethod();
4323    data.delivery_api_point = $("#api_points").val();
4324    data.payment_method = checkSelectedPaymentMethod();
4325    data.discount_code = $("#discount_code").val();
4326    data.special_service_id = $("#special_service").val();
4327    data.special_service_name = $("#special_service").val();
4328    data.comments = $("#comments").val();
4329
4330    if ($('input[name="send_as_gift"]').is(':checked')) {
4331      data.send_as_gift = 1;
4332      data.send_as_gift_value = $("#send_as_gift_value").val();
4333    }
4334
4335    // CV3 (2026-08-12): embrulhos multi — modo/linhas/dedicatória (o preço é
4336    // recalculado no servidor a partir da configuração, nunca daqui).
4337    data.gift_wrap_enabled = $("#gift_wrap_enabled").val() || "0";
4338    data.gift_wrap_mode = $("#gift_wrap_mode").val() || "general";
4339    data.gift_wrap_lines = $("#gift_wrap_lines").val() || "";
4340    data.gift_wrap_dedicatoria = $("#gift_wrap_dedicatoria").val() || "";
4341
4342    return data;
4343  }
4344
4345  function fetchUTMContext() {
4346    try {
4347      return {
4348        utm_source: sessionStorage.getItem("utm_source") || "",
4349        utm_medium: sessionStorage.getItem("utm_medium") || "",
4350        utm_campaign: sessionStorage.getItem("utm_campaign") || "",
4351        referrer: sessionStorage.getItem("referrer") || "",
4352        landing_page: sessionStorage.getItem("landingPage") || ""
4353      };
4354    } catch (err) {
4355      return {
4356        utm_source: "",
4357        utm_medium: "",
4358        utm_campaign: "",
4359        referrer: "",
4360        landing_page: ""
4361      };
4362    }
4363  }
4364
4365  function handleOrderSuccess(rawResponse, orderData) {
4366    var response = {};
4367    if (!rawResponse) return null;
4368    if (typeof rawResponse === "string" && rawResponse.trim() === "mail-err") {
4369      if (typeof swal === "function") {
4370        swal({
4371          title: gl("swal_attention") || "Atenção",
4372          text: gl("EMAILNOK") || "O e-mail que introduziu parece não existir, por favor valide o campo E-mail.",
4373          type: "warning",
4374          confirmButtonColor: "#ABD559",
4375          showCancelButton: false,
4376          animation: false,
4377          confirmButtonText: gl("swal_btnok") || "OK",
4378          closeOnConfirm: true
4379        });
4380      }
4381      return { error: true };
4382    }
4383    if (typeof rawResponse === "string") {
4384      try { response = JSON.parse(rawResponse); }
4385      catch (err) { response = {}; }
4386    } else if (typeof rawResponse === "object") {
4387      response = rawResponse || {};
4388    }
4389
4390    // Idempotência: a primeira execução ainda está a processar (duplo pedido
4391    // simultâneo) — informar e manter a MESMA chave (não é uma nova tentativa)
4392    if (response && response.error === "idem_processing") {
4393      if (typeof toastr !== "undefined") toastr.info(gl("CV2_IDEM_PROCESSING") || "A sua encomenda está a ser processada. Aguarde um instante por favor.");
4394      return { error: true, idemProcessing: true };
4395    }
4396
4397    // CV2 Fase B5: conflito de stock — o gate atomico do processOrder recusou a compra
4398    // (rollBack, nenhum stock abatido, sem cobranca; em express a autorizacao e anulada).
4399    // Mostrar mensagem clara com o artigo e re-marcar o cesto (mantendo os restantes itens).
4400    if (response && response.error === "stock_conflict") {
4401      var scName = (response.name || "").toString();
4402      var scTitle = gl("CV2_SC_TITLE") || "Artigo esgotado";
4403      var scText = (gl("CV2_SC_TEXT") || "A última unidade de {N} esgotou entretanto. A compra não foi concluída.").replace("{N}", scName || "-");
4404      if (typeof swal === "function") {
4405        swal({ title: scTitle, text: scText, type: "warning", confirmButtonColor: "#8e2447", showCancelButton: fal
4405se, animation: false, confirmButtonText: gl("swal_btnok") || "OK", closeOnConfirm: true });
4406      } else if (typeof toastr !== "undefined") {
4407        toastr.error(scText);
4408      }
4409      // recomputar totais (o esgotado ja tem stock 0 -> excluido server-side) e re-marcar
4410      // o estado de stock no cesto/resumo (badge Esgotado + bloqueio do botao de compra)
4411      // t1570: regenerar HTML das linhas — senão o resumo fica com preços stale e totais a 0
4412      try { if (typeof window.generate_shopcart === "function") window.generate_shopcart("html"); } catch (e) {}
4413      try { if (typeof window.populate_shopcart === "function") window.populate_shopcart(); } catch (e) {}
4414      try { if (typeof window.cv2StockUxRefresh === "function") window.cv2StockUxRefresh(); } catch (e) {}
4415      return { error: true, stockConflict: true };
4416    }
4417
4418    var general = response.general || {};
4419    var paymentMethod = orderData.payment_method;
4420
4421    // Rotas reais (2026-08-14): metodos assincronos saem do /checkout/ para
4422    // /order-received/. IMPORTANTE (2026-08-15): sinalizar a saida ANTES de
4423    // navegar — sem isto o check_shopcart pos-encomenda esvazia o cesto e o
4424    // bootCv2EmptyWatch faz location.replace("/pt/") que CANCELA a navegacao
4425    // (causa real do "fui parar a homepage" no teste MBWAY do dono).
4426    var orderPageToken = general.order_token || "";
4427    if (orderPageToken && (paymentMethod === "sc_payment_sibs_mb" || paymentMethod === "sc_payment_transfer" || paymentMethod === "sc_payment_reception")) {
4428      var opLang = $("#session_lang").val() || "pt";
4429      window.__cv2Leaving = true;
4430      response.cv2Redirecting = true;
4431      window.location.href = "/" + opLang + "/order-received/?o=" + orderPageToken;
4432      return response;
4433    }
4434
4435    // MBWAY (2026-08-15): PERMANECE NO CHECKOUT — painel de espera integrado na
4436    // seccao de pagamento + polling autoritativo; aprovado -> /order-confirmed/;
4437    // recusado/cancelado/expirado -> mensagem + outro metodo (repay, MESMA
4438    // encomenda); desconhecido -> continuar a aguardar (nunca 2a cobranca).
4439    if (paymentMethod === "sc_payment_sibs_mbway" && orderPageToken) {
4440      var mbLang = $("#session_lang").val() || "pt";
4441      // o legacy pode devolver a resposta MBWAY em sibs_mbway OU em sibs_card
4442      // (reutilizacao historica da variavel no servidor)
4443      var mbInitObj = response.sibs_mbway || response.sibs_card || null;
4444      var mbInitCode = (mbInitObj && mbInitObj.result && mbInitObj.result.code) ? String(mbInitObj.result.code) : "";
4445      var mbInitOk = /^(000\.(000|200)\.|800\.400\.5)/.test(mbInitCode);
4446      if (typeof window.cv2MbwayCheckoutWait === "function") {
4447        window.__cv2Leaving = true; // suspende watchers de cesto vazio
4448        response.cv2Redirecting = true; // nao refrescar o cesto por cima do painel
4449        var mbPhone = $.trim($("#sc_payment_sibs_mbway .checkout-modern__mbway-input").val() || "");
4450        window.cv2MbwayCheckoutWait({
4451          token: orderPageToken,
4452          orderId: general.order_id || "",
4453          lang: mbLang,
4454          initOk: mbInitOk,
4455          initCode: mbInitCode,
4456          phone: mbPhone,
4457          repayUrl: "/" + mbLang + "/repay/" + (general.order_id || "") + "/"
4458        });
4459        return response;
4460      }
4461      // fallback (modulo indisponivel): pagina order-received
4462      window.__cv2Leaving = true;
4463      response.cv2Redirecting = true;
4464      window.location.href = "/" + mbLang + "/order-received/?o=" + orderPageToken;
4465      return response;
4466    }
4467
4468    if (paymentMethod === "sc_payment_sibs_card" && response.sibs_card && response.sibs_card.id) {
4469      var sibsLang = $("#session_lang").val() || "pt";
4470      // Fica no /checkout/ com COPYandPAY — sinalizar saida do fluxo de cesto
4471      // (como MBWAY) para o empty-watch nao mandar para a homepage quando o
4472      // processOrder esvazia NEWPROD. Refresh reabre o passo via shopcart.php.
4473      window.__cv2Leaving = true;
4474      response.cv2Redirecting = true; // nao regenerar cesto por cima do widget
4475      $.ajaxSetup({ cache: true });
4476      window.wpwlOptions = { locale: sibsLang, paymentTarget: "_top", style: "plain" };
4477      $("#paymentform").attr("data-brands", "VISA VISAELECTRON VISADEBIT MASTER MASTERDEBIT MAESTRO AMEX");
4478      // retorno do widget: handler autoritativo (valida na SIBS antes de decidir destino)
4479      if (orderPageToken) $("#paymentform").attr("action", "https://" + window.location.host + "/" + sibsLang + "/payment-return/?o=" + orderPageToken);
4480      $("#paymentinfo").html('<h1>' + (gl("SIBS_CARD") || "Pagamento com Cartão") + "</h1>");
4481      $.getScript("https://eu-prod.oppwa.com/v1/paymentWidgets.js?checkoutId=" + response.sibs_card.id);
4482    }
4483
4484    if (paymentMethod === "sc_payment_sibs_mb") {
4485      // legacy: o cartao de confirmacao ja mostra Entidade/Referencia/Valor -> limpar o
4486      // titulo antigo do #paymentinfo que aparecia solto no topo ("Dados para pagamento MB")
4487      $("#paymentinfo").empty();
4488    }
4489
4490    if (paymentMethod === "sc_payment_sibs_mbway") {
4491      // legacy: o cartao de confirmacao ja mostra o metodo (MB WAY) -> nao deixar o
4492      // titulo <h1>MB WAY</h1> solto no topo do __main (mesma logica do MB acima)
4493      $("#paymentinfo").empty();
4494    }
4495
4496    if (paymentMethod === "sc_payment_api_paypal" && response.paypal) {
4497      $.ajax({
4498        type: "POST",
4499        cache: false,
4500        url: REQUEST_ENDPOINT,
4501        data: { ajax: "paypal_soap", wsData: response.paypal },
4502        success: function (data) {
4503          try {
4504            var parsed = typeof data === "string" ? JSON.parse(data) : data;
4505            if (parsed && parsed.redirectUrl) window.location.href = parsed.redirectUrl;
4506          } catch (err) { }
4507        }
4508      });
4509    }
4510
4511    if (paymentMethod === "sc_payment_api" || paymentMethod === "sc_payment_api_paypal") {
4512      $(".hdrT1").hide();
4513    }
4514
4515    $(window).scrollTop(0);
4516    $("#shopcart_login, #shopcart_html, #shopcart_cupidovip, #optionals_table, #encomenda, #payment_methods, #comments_table, #shop_cart_footer, #shop_cart_footer_info").hide();
4517    $(".hdrT2, .hdrT3, #footer_container").hide();
4518    $("#hipay_form_container").fadeIn(1200);
4519
4520    var successHtml = "";
4521    if (general.html_success) successHtml += general.html_success;
4522    if (general.template_body) successHtml += general.template_body;
4523
4524    if (successHtml) {
4525      $("#order_info").html(successHtml).fadeIn(1200);
4526    }
4527
4528    var currency = ($("#session_currency").val() || "EUR");
4529    $(".basketTotalId").text(currency + " 0.00");
4530    $(".basketTotalIdMobile").text(currency + " 0.00");
4531    $(".basketId").text("0 " + gl("ITEMS"));
4532    $(".basketIdMobile").text("0");
4533					$("body").addClass("checkout-modern-cart-empty");
4534					$(".checkout-modern__cart-aside").hide();
4535
4536    return response;
4537  }
4538
4539  // CV2 (2026-09-21+): bloquear pagamento sem morada (home/alt) e/ou telemóvel
4540  function cv2GlOrFallback(key, fallback) {
4541    var v = (typeof gl === "function") ? gl(key) : "";
4542    if (!v || v === key || v === "[" + key + "]") return fallback;
4543    return v;
4544  }
4545  function cv2CheckoutAddressMsg() {
4546    return cv2GlOrFallback(
4547      "CV2_SHIPPING_ADDRESS_REQUIRED",
4548      "Indique a morada de entrega completa (nome, morada, código postal, localidade e país) antes de pagar."
4549    );
4550  }
4551  function cv2CheckoutPhoneMsg() {
4552    return cv2GlOrFallback(
4553      "CV2_SHIPPING_PHONE_REQUIRED",
4554      "Indique um telemóvel válido (9 a 15 dígitos) antes de pagar."
4555    );
4556  }
4557  function cv2PhoneOk(v) {
4558    var s = String(v == null ? "" : v).replace(/[\s\-().]/g, "");
4559    if (s.charAt(0) === "+") s = s.slice(1);
4560    return /^[0-9]{9,15}$/.test(s);
4561  }
4562  function cv2CheckoutPhoneValue() {
4563    return $("#cacc_telefone").val() || $("#telefone").val() || $("#phone").val() || "";
4564  }
4565  function cv2OpenAccountAddressForm(focusPhone) {
4566    if ($("#cacc_form").length) {
4567      $("#cacc_view").hide();
4568      $("#cacc_form").show();
4569      var $t = focusPhone ? $("#cacc_telefone") : $("#cacc_morada");
4570      if (!$t.length) $t = $("#cacc_telefone").length ? $("#cacc_telefone") : $("#cacc_morada");
4571      if ($t.length) {
4572        try { $("html, body").animate({ scrollTop: $t.offset().top - 110 }, 400); } catch (e1) {}
4573        $t.focus();
4574      }
4575    }
4576  }
4577  function cv2CheckoutHasShippingAddress() {
4578    var dm = (typeof checkSelectedDeliveryMethod === "function") ? checkSelectedDeliveryMethod() : "";
4579    function filled(v) { return $.trim(String(v == null ? "" : v)).length > 0; }
4580    // CTT/Worten: sem morada de rua; telemóvel de contacto (regra guest)
4581    if (dm === "sc_delivery_ctt" || dm === "sc_delivery_worten") {
4582      return cv2PhoneOk(cv2CheckoutPhoneValue());
4583    }
4584    var addrOk = false;
4585    if (dm === "sc_delivery_alternative") {
4586      addrOk = filled($("#delivery_nome").val()) && filled($("#delivery_morada").val()) &&
4587        filled($("#delivery_localidade").val()) && filled($("#delivery_codigo_postal").val()) &&
4588        filled($("#delivery_country").val());
4589    } else if ($("#cacc_morada").length) {
4590      addrOk = filled($("#cacc_nome").val()) && filled($("#cacc_morada").val()) &&
4591        filled($("#cacc_localidade").val()) && filled($("#cacc_cp").val()) &&
4592        filled($("#cacc_country").val());
4593    } else {
4594      addrOk = filled($("#nome").val()) && filled($("#morada").val()) &&
4595        filled($("#localidade").val()) && filled($("#codigo_postal").val()) &&
4596        filled($("#country").val());
4597    }
4598    return addrOk && cv2PhoneOk(cv2CheckoutPhoneValue());
4599  }
4600  function cv2CheckoutMissingPhoneOnly() {
4601    var dm = (typeof checkSelectedDeliveryMethod === "function") ? checkSelectedDeliveryMethod() : "";
4602    function filled(v) { return $.trim(String(v == null ? "" : v)).length > 0; }
4603    if (dm === "sc_delivery_ctt" || dm === "sc_delivery_worten") return !cv2PhoneOk(cv2CheckoutPhoneValue());
4604    var addrOk = false;
4605    if (dm === "sc_delivery_alternative") {
4606      addrOk = filled($("#delivery_nome").val()) && filled($("#delivery_morada").val()) &&
4607        filled($("#delivery_localidade").val()) && filled($("#delivery_codigo_postal").val()) &&
4608        filled($("#delivery_country").val());
4609    } else if ($("#cacc_morada").length) {
4610      addrOk = filled($("#cacc_nome").val()) && filled($("#cacc_morada").val()) &&
4611        filled($("#cacc_localidade").val()) && filled($("#cacc_cp").val()) &&
4612        filled($("#cacc_country").val());
4613    } else {
4614      addrOk = filled($("#nome").val()) && filled($("#morada").val()) &&
4615        filled($("#localidade").val()) && filled($("#codigo_postal").val()) &&
4616        filled($("#country").val());
4617    }
4618    return addrOk && !cv2PhoneOk(cv2CheckoutPhoneValue());
4619  }
4620
4621  function submitOrder() {
4622    // espera MBWAY ativa: a encomenda JA existe e o pedido esta na app — nada
4623    // pode voltar a submeter (o disabled do botao nao chega: proxies com
4624    // .trigger('click') disparam handlers jQuery mesmo em botoes disabled, e
4625    // um populate_shopcart podia reativa-lo)
4626    if (window.__cv2MbwayWaitActive) return false;
4627    // remover o ultimo artigo do cesto NUNCA pode criar encomenda
4628    if (window.__cv2UserEmptiedCart || window.__cv2CartRemoving || $("#shopcart_empty").length) {
4629      if (typeof toastr !== "undefined") toastr.info(gl("CESTO_VAZIO1") || "O cesto esta vazio");
4630      return false;
4631    }
4632    // Sample-only / sem produto normal (restauro check_amostras + CONFIGURAÇÕES)
4633    if ($("#check_amostras").length && (parseInt($("#check_amostras").val(), 10) || 0) < 1) {
4634      var cv2SamplesOnlyMsg = ($("#check_amostras").attr("data-samples-only") === "1")
4635        ? (gl("CV2_SAMPLES_ONLY_CHECKOUT") || "Não é possível finalizar uma encomenda só com amostras. Adicione pelo menos um produto da loja.")
4636        : (gl("COMPRAS_A_ZERO") || "O seu carrinho está vazio.");
4637      if (typeof toastr !== "undefined") toastr.error(cv2SamplesOnlyMsg);
4638      else if (typeof Swal !== "undefined") Swal.fire({ text: cv2SamplesOnlyMsg, icon: "info" });
4639      else alert(cv2SamplesOnlyMsg);
4640      return false;
4641    }
4642    if (!cv2CheckoutHasShippingAddress()) {
4643      var phoneOnly = (typeof cv2CheckoutMissingPhoneOnly === "function") && cv2CheckoutMissingPhoneOnly();
4644      cv2OpenAccountAddressForm(!!phoneOnly);
4645      var msg = phoneOnly ? cv2CheckoutPhoneMsg() : cv2CheckoutAddressMsg();
4646      if (typeof toastr !== "undefined") toastr.error(msg);
4647      else if (typeof swal === "function") {
4648        swal({ title: gl("swal_attention") || "Atenção", text: msg, type: "warning", confirmButtonColor: "#ABD559", confirmButtonText: gl("swal_btnok") || "OK" });
4649      } else { alert(msg); }
4650      return false;
4651    }
4652    if (!$('input[name="terms"]').is(":checked")) {
4653      if (typeof swal === "function") {
4654        swal({
4655          title: gl("swal_attention") || "Atenção",
4656          text: gl("swal_missing_fields") || "Tem de aceitar os termos.",
4657          type: "warning",
4658          confirmButtonColor: "#ABD559",
4659          showCancelButton: false,
4660          animation: false,
4661          confirmButtonText: gl("swal_btnok") || "OK",
4662          closeOnConfirm: true
4663        });
4664      } else {
4665        alert(gl("swal_missing_fields") || "Tem de aceitar os termos.");
4666      }
4667      return false;
4668    }
4669
4670    setSubmitLoading(true);
4671
4672    var populatePayload = gatherShopcartRequestPayload();
4673
4674    $.ajax({
4675      type: "POST",
4676      cache: false,
4677      url: REQUEST_ENDPOINT,
4678      data: populatePayload
4679    })
4680    .done(function (response) {
4681      var shopcartData = {};
4682      if (typeof response === "string") {
4683        try { shopcartData = JSON.parse(response); }
4684        catch (err) { shopcartData = {}; }
4685      } else if (typeof response === "object") {
4686        shopcartData = response || {};
4687      }
4688
4689      var formData = gatherOrderFormData();
4690      var orderData = gatherOrderData();
4691      var utmData = fetchUTMContext();
4692
4693      $.ajax({
4694        type: "POST",
4695        cache: false,
4696        url: REQUEST_ENDPOINT,
4697        data: $.extend({
4698          ajax: "processOrder",
4699          orderData: orderData,
4700          formData: formData,
4701          shopcartData: shopcartData
4702        }, utmData)
4703      })
4704      .done(function (result) {
4705        var parsed = handleOrderSuccess(result, orderData);
4706        if (!parsed || parsed.error) {
4707          setSubmitLoading(false);
4708          // falha de NEGÓCIO confirmada pelo servidor -> a próxima submissão é
4709          // uma nova tentativa lógica (chave nova). Em idem_processing a chave
4710          // mantém-se (a primeira execução ainda decorre).
4711          if (!parsed || parsed.idemProcessing !== true) cv2IdemKey(true);
4712          if (parsed && parsed.stockConflict) {
4713            // ja tratado em handleOrderSuccess (mensagem clara + refresh do cesto)
4714          } else if (typeof toastr !== "undefined") {
4715            if (parsed && (parsed.error === "shipping_address_required" || parsed.error === "shipping_phone_required")) {
4716              cv2OpenAccountAddressForm(parsed.error === "shipping_phone_required");
4717            }
4718            if (parsed && parsed.message) {
4719              var cv2ErrMsg = parsed.message;
4720              if (parsed.error === "shipping_address_required" || cv2ErrMsg === "CV2_SHIPPING_ADDRESS_REQUIRED") {
4721                cv2ErrMsg = cv2CheckoutAddressMsg();
4722              } else if (parsed.error === "shipping_phone_required" || cv2ErrMsg === "CV2_SHIPPING_PHONE_REQUIRED") {
4723                cv2ErrMsg = cv2CheckoutPhoneMsg();
4724              }
4725              if (parsed.products && parsed.products.length) cv2ErrMsg += " " + parsed.products.join(", ") + ".";
4726              toastr.error(cv2ErrMsg);
4727            } else {
4728              toastr.error(gl("swal_missing_fields") || "Ocorreu um erro ao processar a encomenda.");
4729            }
4730          }
4731        } else {
4732          // em navegacao/espera pos-encomenda NAO refrescar o cesto: o refresh
4733          // esvazia a UI e os watchers de cesto-vazio mandavam para a homepage
4734          if (!parsed.cv2Redirecting) check_shopcart();
4735        }
4736      })
4737      .fail(function () {
4738        setSubmitLoading(false);
4739        if (typeof toastr !== "undefined") toastr.error("Não foi possível processar a encomenda.");
4740      });
4741    })
4742    .fail(function () {
4743      setSubmitLoading(false);
4744      if (typeof toastr !== "undefined") toastr.error("Não foi possível validar o carrinho.");
4745    });
4746
4747    return false;
4748  }
4749
4750  /* t1407: telemóvel PT/intl — number:true rejeitava +351... */
4751  function cv2EnsurePhoneValidator() {
4752    if (typeof jQuery === "undefined" || !jQuery.validator || jQuery.validator.methods.cv2phone) return;
4753    jQuery.validator.addMethod("cv2phone", function (value, element) {
4754      if (this.optional(element)) return true;
4755      var s = String(value || "").replace(/[\s\-().]/g, "");
4756      if (s.charAt(0) === "+") s = s.slice(1);
4757      return /^[0-9]{9,15}$/.test(s);
4758    }, "Introduza um telemóvel válido.");
4759  }
4760
4761  function initOrderForm() {
4762    var $form = $("#encomenda");
4763    if (!$form.length || typeof $.fn.validate === "undefined") return;
4764    cv2EnsurePhoneValidator();
4765
4766    $form.off("submit.orderForm").on("submit.orderForm", function (e) {
4767      e.preventDefault();
4768    });
4769
4770    $("#shopcart_submit").off("click.order").on("click.order", function (e) {
4771      e.preventDefault();
4772      if ($form.valid && $form.valid()) {
4773        submitOrder();
4774      } else {
4775        $form.submit();
4776      }
4777    });
4778
4779    if ($form.data("validator")) return;
4780
4781    $form.validate({
4782      ignore: ":not(:visible)",
4783      focusInvalid: false,
4784      rules: {
4785        delivery_nome: { required: true },
4786        delivery_morada: { required: true },
4787        delivery_localidade: { required: true },
4788        delivery_codigo_postal: { required: true },
4789        delivery_provincia: { required: true },
4790        provincia: { required: true },
4791        nome: { required: true },
4792        email: { required: true, email: true },
4793        email2: { required: true, equalTo: "#email" },
4794        password: { required: true, minlength: 6 },
4795        password2: { equalTo: "#password" },
4796        morada: { required: true },
4797        localidade: { required: true },
4798        codigo_postal: { required: true },
4799        telefone: { required: true, cv2phone: true },
4800        api_points: { required: true }
4801      },
4802      invalidHandler: function (event, validator) {
4803        /* t1589: sem popup — só highlight + scroll/foco no 1.º campo inválido */
4804        if (validator && validator.errorList && validator.errorList.length) {
4805          var el = validator.errorList[0].element;
4806          $("html, body").animate({ scrollTop: $(el).offset().top - 110 }, 1000);
4807          $(el).focus().trigger("click");
4808        }
4809      },
4810      onkeyup: function (element) {
4811        $(element).removeClass("LV_invalid_input");
4812      },
4813      onfocusout: function (element) {
4814        var $el = $(element);
4815        if ($el.valid && $el.valid()) {
4816          if (element.id === "api_points") $("#api_points_container").removeClass("LV_invalid_input");
4817          else $el.removeClass("LV_invalid_input");
4818        } else {
4819          if (element.id === "api_points") {
4820            /* t1589: sem popup no focusout — só highlight + label */
4821            $("#api_points_container").addClass("LV_invalid_input");
4822          } else {
4823            $el.addClass("LV_invalid_input");
4824          }
4825        }
4826      },
4827      messages: (window.cv2Jqv && cv2Jqv.fieldMessages) ? cv2Jqv.fieldMessages() : {},
4828      errorPlacement: function (error, element) {
4829        if (window.cv2Jqv && cv2Jqv.errorPlacement) return cv2Jqv.errorPlacement(error, element);
4830        error.insertAfter(element);
4831      },
4832      showErrors: function (errorMap, errorList) {
4833        if (window.cv2Jqv && cv2Jqv.showErrorsWithLabels) {
4834          return cv2Jqv.showErrorsWithLabels.call(this, errorMap, errorList);
4835        }
4836        $.each(errorList, function (_, item) {
4837          $(item.element).addClass("LV_invalid_input");
4838        });
4839        this.defaultShowErrors();
4840      },
4841      submitHandler: function () {
4842        submitOrder();
4843      }
4844    });
4845  }
4846
4847  /* Quick Add + navegação URL legada (mantidos) */
4848  $(function () {
4849    $("body").on("click.nav", ".webstore-nav-bottom", function () {
4850      $('html, body').animate({ scrollTop: $("#catalog_top").offset().top }, 1000);
4851    });
4852
4853    $("body").on("click.qa", ".btnQuickAdd", function () {
4854      var elementos = $(this).attr("elementos");
4855      var id_code   = $(this).attr("id_code");
4856      var furl      = $(this).attr("furl");
4857      var lang      = $(this).attr("lang");
4858      if (Number(elementos) > 0) {
4859        window.location.href = window.location.origin + '/' + lang + '/prod/' + furl + '/' + id_code + '/';
4860      } else {
4861        $.post(REQUEST_ENDPOINT, { ajax: "shopcart_add", id_code: id_code }, function () {
4862          check_shopcart(); /* t1592: sem toast SHOPCART_ADDED_OK */
4863        });
4864      }
4865    });
4866  });
4867
4868
4869  /* ==========================================================
4870   * 10) CESTO NO TOPO
4871   * ========================================================== */
4872
4873		function updateHeaderCart(data) {
4874			if (!data) {
4875				// pedido leve ao backend se não for passado o objeto
4876				return $.ajax({
4877					type: "POST",
4878					url: REQUEST_ENDPOINT,
4879					data: { ajax: "populate_shopcart" },
4880					dataType: "json",
4881					cache: false
4882				}).done(updateHeaderCart);
4883			}
4884
4885			var items = parseInt(data.all_total_items || data.total_items || data.items || 0, 10);
4886			var itemText = items + " " + (items === 1 ? gl("ITEM") : gl("ITEMS"));
4887
4888			// Desktop → com texto / Mobile → só número
4889			$(".basketId").text(itemText);
4890			$(".basketIdMobile").text(items);
4891
4892			var cur = ($("#session_currency").val() || data.currency || "EUR").toUpperCase();
4893			var SYM = { EUR: "€", USD: "$", GBP: "£", BRL: "R$" };
4894			var total = data.bigtotal_cambio || data.subtotal_cambio || data.bigtotal || data.subtotal || 0;
4895			var formatted = (SYM[cur] || cur + " ") + parseFloat(total || 0).toFixed(2).replace(".", ",");
4896
4897			$(".basketTotalId, .basketTotalIdMobile").text(formatted);
4898		}
4899
4900		/* ===== Check cart (header + mini-cart + legacy layout) ===== */
4901		function check_shopcart(mode) {
4902			$.ajax({
4903				type: "POST",
4904				url:  REQUEST_ENDPOINT + "?ts=" + Date.now(),   // cache buster
4905				data: { ajax: "check_shopcart" },
4906				cache: false
4907			})
4908			.done(function (response) {
4909				var dataObject = null;
4910				if (response && typeof response === "object") {
4911					dataObject = response;
4912				} else if (typeof response === "string") {
4913					try { dataObject = JSON.parse(response); }
4914					catch (err) { dataObject = null; }
4915				}
4916
4917				var raw = (typeof response === "string") ? response.trim() : response;
4918				var hasCart = Array.isArray(dataObject) ? dataObject.length > 0 : !!(raw && raw !== "null");
4919
4920				// Atualiza cabeçalho com os dados recebidos; se não vierem, faz fallback
4921				if (dataObject) updateHeaderCart(dataObject);
4922				else updateHeaderCart();
4923
4924				if (hasCart) {
4925					$("body").removeClass("checkout-modern-cart-empty");
4926					$(".checkout-modern__cart-aside").show();
4927					// Fluxo legado quando estamos a iniciar o checkout completo
4928					if (mode === "init") {
4929						var uid = parseInt($("#uid").val(), 10) || 0;
4930						if (uid > 0 && typeof generate_cupidovip === "function") {
4931							generate_cupidovip(uid);
4932							$("#shopcart_cupidovip").stop(true, true).show();
4933						} else {
4934							$("#shopcart_login").show();
4935						}
4936
4937						var fadeInSelectors = [
4938							"#delivery_methods",
4939							"#shop_cart_table_form",
4940							"#payment_methods",
4941							"#comments_table",
4942							"#gift_option_table",
4943							"#gift_message_table",
4944							"#optionals_table",
4945							"#shop_cart_footer",
4946							"#shop_cart_footer_info",
4947							"#shop_cart_footer_iva"
4948						];
4949
4950						fadeInSelectors.forEach(function (selector) {
4951							$(selector).stop(true, true).fadeIn(500);
4952						});
4953					}
4954
4955					if (typeof populate_shopcart === "function") {
4956						populate_shopcart();
4957					}
4958
4959					if (typeof shopcart_specialservices === "function") {
4960						shopcart_specialservices();
4961					}
4962
4963					if (typeof generate_shopcart_modal === "function") {
4964						generate_shopcart_modal();
4965					}
4966				} else {
4967					// t1683: on checkout-v2, do NOT fadeOut Conta/Entrega/Payment —
4968					// empty check_shopcart can race after sections were shown.
4969					var isCheckoutV2 = $(".checkout-modern.checkout-v2").length > 0;
4970					if (!isCheckoutV2) {
4971						var fadeOutSelectors = [
4972							"#shopcart_cupidovip",
4973							"#shopcart_login",
4974							"#delivery_methods",
4975							"#shop_cart_table_form",
4976							"#payment_methods",
4977							"#comments_table",
4978							"#gift_option_table",
4979							"#gift_message_table",
4980							"#optionals_table",
4981							"#shop_cart_footer",
4982							"#shop_cart_footer_info",
4983							"#shop_cart_footer_iva"
4984						];
4985
4986						fadeOutSelectors.forEach(function (selector) {
4987							$(selector).stop(true, true).fadeOut(500);
4988						});
4989						$(".checkout-modern__cart-aside").hide();
4990					}
4991
4992					var currency = ($("#session_currency").val() || "EUR").toUpperCase();
4993					$(".basketTotalId").text(currency + " 0.00");
4994					$(".basketTotalIdMobile").text(currency + " 0.00");
4995					$(".basketId").text("0 " + gl("ITEMS"));
4996					$(".basketIdMobile").text("0");
4997					$("body").addClass("checkout-modern-cart-empty");
4998				}
4999
5000				// Se o mini-cart estiver visível, refresca o HTML
5001				var $mc = $("#shopcart-modal-container, #mini-cart");
5002				if ($mc.is(":visible")) {
5003					$.post(REQUEST_ENDPOINT, { ajax: "generate_shopcart", type: "html" })
5004					.done(function (html) {
5005						if (typeof window.renderShopcartDrawerHtml === "function") {
5006							window.renderShopcartDrawerHtml(html);
5007						} else if ($("#shopcart-modal").length) {
5008							$("#shopcart-modal").html(html);
5009						} else {
5010							$mc.html(html);
5011						}
5012					});
5013				}
5014			})
5015			.fail(function (xhr) {
5016				console.debug("check_shopcart FAIL", xhr.status, xhr.responseText);
5017			});
5018
5019			if (mode === "init") {
5020				$("#footer_container").stop(true, true).fadeIn(1500);
5021			}
5022		}
5023		window.check_shopcart = check_shopcart;
5024
5025    /* ==========================================================
5026     * MINI-CART DRAWER
5027     * ========================================================== */
5028    function bindMiniCartHover() {
5029      var $desktopPanel = $("#shopcart-modal-container");
5030      if (!$desktopPanel.length) return;
5031      if (!$desktopPanel.parent().is("body")) {
5032        $desktopPanel.appendTo("body");
5033        $desktopPanel = $("#shopcart-modal-container");
5034      }
5035
5036      if (!$("#shopcart-drawer-overlay").length) {
5037        $("body").append("<div id=\"shopcart-drawer-overlay\" class=\"shopcart-drawer-overlay\"></div>");
5038      }
5039
5040      var SHOPCART_SAVED_COLLAPSED_KEY = "cupido_shopcart_saved_for_later_collapsed";
5041
5042      function currentPanel() {
5043        return $desktopPanel;
5044      }
5045
5046      function isSavedForLaterCollapsed() {
5047        try { return localStorage.getItem(SHOPCART_SAVED_COLLAPSED_KEY) === "1"; } catch (e) { return false; }
5048      }
5049
5050      function setSavedForLaterCollapsed(collapsed) {
5051        try { localStorage.setItem(SHOPCART_SAVED_COLLAPSED_KEY, collapsed ? "1" : "0"); } catch (e) {}
5052      }
5053
5054      function applySavedForLaterState($slot) {
5055        var $section = ($slot && $slot.length ? $slot : $currentSavedSlot()).find(".shopcart-drawer__saved-section");
5056        if (!$section.length) return;
5057        var empty = currentPanel().find(".shopcart_empty, .shopcart-drawer__empty").length > 0;
5058        var savedCount = $section.find(".shopcart-drawer__saved-item").length;
5059        if (!savedCount) {
5060          var badge = parseInt($.trim($section.find(".shopcart-drawer__saved-head strong").first().text()), 10);
5061          if (!isNaN(badge)) savedCount = badge;
5062        }
5063        if (empty && savedCount > 0) {
5064          $section.removeClass("is-collapsed");
5065          return;
5066        }
5067        $section.toggleClass("is-collapsed", empty || isSavedForLaterCollapsed());
5068      }
5069
5070      function setShopcartDrawerLoading(active, $scope) {
5071        var $target = ($scope && $scope.length) ? $scope : currentPanel();
5072        if (!$target || !$target.length) return;
5073        if (!$target.find(".shopcart-drawer__loader").length) {
5074          $target.append("<div class=\"shopcart-drawer__loader\" aria-hidden=\"true\"><span></span></div>");
5075        }
5076        $target.toggleClass("is-shopcart-loading", !!active);
5077        $target.attr("aria-busy", active ? "true" : "false");
5078      }
5079      window.setShopcartDrawerLoading = setShopcartDrawerLoading;
5080
5081      function drawerLabels() {
5082        var l = window.CUPIDO_CART_DRAWER_LABELS || {};
5083        return {
5084          eyebrow: l.eyebrow || gl("SHOPCART_DRAWER_EYEBROW") || "Cesto de compras",
5085          title: l.title || gl("SHOPCART_DRAWER_TITLE") || (currentLangCode() === "en" ? "Bag" : currentLangCode() === "es" ? "Cesta" : "Cesto"),
5086          close: l.close || gl("SHOPCART_DRAWER_CLOSE") || "Fechar",
5087          subtotal: l.subtotal || gl("SHOPCART_DRAWER_SUBTOTAL") || "Subtotal",
5088          shipping: l.shipping || gl("SHOPCART_DRAWER_SHIPPING") || "Portes",
5089          shippingPending: l.shippingPending || gl("SHOPCART_DRAWER_SHIPPING_PENDING") || "A calcular",
5090          savings: l.savings || gl("SHOPCART_DRAWER_SAVINGS") || "Descontos",
5091          total: l.total || gl("SHOPCART_DRAWER_TOTAL") || "Total",
5092          checkout: l.checkout || gl("SHOPCART_DRAWER_CHECKOUT") || "CONTINUAR PARA O CHECKOUT",
5093          cart: l.cart || gl("SHOPCART_DRAWER_CART") || "IR PARA O CESTO",
5094          freeShippingFrom: l.freeShippingFrom || gl("SHOPCART_DRAWER_FREE_SHIPPING_FROM") || "Portes grátis em {N} a partir de",
5095          freeShippingRemaining: l.freeShippingRemaining || gl("SHOPCART_DRAWER_FREE_SHIPPING_REMAINING") || "faltam para portes grátis",
5096          freeShippingRemaining2: l.freeShippingRemaining2 || gl("SHOPCART_DRAWER_FS_REMAINING2") || "Faltam %s para portes grátis",
5097          freeShippingQualified: l.freeShippingQualified || gl("SHOPCART_DRAWER_FREE_SHIPPING_QUALIFIED") || "Já tem portes grátis para Portugal Continental",
5098          shippingFree: l.shippingFree || gl("SHOPCART_DRAWER_SHIPPING_FREE") || "Grátis",
5099          totalEstimated: l.totalEstimated || gl("SHOPCART_DRAWER_TOTAL_ESTIMATED") || "Total estimado",
5100          plusShipping: l.plusShipping || gl("SHOPCART_DRAWER_PLUS_SHIPPING") || "+ portes",
5101          expressNote: l.expressNote || gl("CV2_DX_NOTE") || "O valor final e os portes são confirmados após selecionar a morada.",
5102          promoQuestion: l.promoQuestion || gl("SHOPCART_DRAWER_PROMO_Q") || "Tem um código de desconto?",
5103          promoPlaceholder: l.promoPlaceholder || gl("SHOPCART_CODIGODESCONTO") || "Código de desconto",
5104          promoApply: l.promoApply || gl("CHECKOUT_APPLY_DISCOUNT_BUTTON") || "Aplicar",
5105          promoRemove: l.promoRemove || gl("CHECKOUT_SUMMARY_REMOVE_CODE") || "Remover",
5106          shippingCalc: l.shippingCalc || gl("SHOPCART_DRAWER_CALC") || "Calcular",
5107          expressTitle: l.expressTitle || gl("CV2_EXPRESS_TITLE") || "Express checkout",
5108          expressOr: l.expressOr || gl("CV2_EXPRESS_OR") || "ou continuar com checkout normal"
5109        };
5110      }
5111
5112      function drawerCurrency() {
5113        return ($("#session_currency").val() || "€").trim() || "€";
5114      }
5115
5116      function buildDrawerItems($content) {
5117        var rows = [];
5118        $content.find("tr").each(function () {
5119          if ($(this).find(".shopcart_remove").length) rows.push(this);
5120        });
5121
5122        if (!rows.length) {
5123          return $content.find(".shopcart_empty, .shopcart_empty_modal").first().length ? $content.html() : "";
5124        }
5125
5126        var items = [];
5127        $.each(rows, function (_, row) {
5128          var $td = $(row).children("td");
5129          var $remove = $td.eq(0).find(".shopcart_remove").first().clone();
5130          var $img = $td.eq(1).find("img").first().clone();
5131          var $detail = $td.eq(2).clone();
5132          var discountHtml = $.trim($td.eq(3).html() || "");
5133          var $qty = $td.eq(4).find("select").first().clone();
5134          var unitPrice = $.trim($td.eq(5).text() || "");
5135          var lineTotal = $.trim($td.last().text() || "");
5136          // layout PV: strike em cima; badge -% + preco por baixo (coluna a direita do titulo)
5137          var $dxDisc = $("<div></div>").html(discountHtml);
5138          var dxStrike = $dxDisc.find("strike").length ? "<span class=\"pvx-strike\">" + $.trim($dxDisc.find("strike").first().text()) + "</span>" : "";
5139          $dxDisc.find("strike").remove();
5140          var dxPct = $.trim($dxDisc.text());
5141          var pricesPart = "<span class=\"shopcart-drawer__prices\">" + dxStrike +
5142            "<span class=\"pvx-now\">" + (dxPct ? "<span class=\"pvx-badge\">" + dxPct + "</span>" : "") +
5143            (unitPrice ? "<span class=\"pvx-price\">" + unitPrice + "</span>" : "") + "</span></span>";
5144          var totalPart = lineTotal ? "<strong class=\"shopcart-drawer__price-total\">" + lineTotal + "</strong>" : "";
5145          // linha oferta (total 0 com unitario > 0): badge OFERTA, sem precos
5146          var dxGiftT = (lineTotal || "").match(/([0-9]+)[.,]([0-9]{2})/);
5147          var dxGiftU = (unitPrice || "").match(/([0-9]+)[.,]([0-9]{2})/);
5148          if (dxGiftT && parseFloat(dxGiftT[1] + "." + dxGiftT[2]) === 0 && dxGiftU && parseFloat(dxGiftU[1] + "." + dxGiftU[2]) > 0) {
5149            var dxGiftLbl = "OFERTA";
5150            if (window.gl) { var dxGiftG = window.gl("CV2_OFFER_BADGE"); if (dxGiftG && dxGiftG !== "CV2_OFFER_BADGE") dxGiftLbl = dxGiftG; }
5151            pricesPart = "<span class=\"shopcart-drawer__prices\"><span class=\"pvx-now\"><span class=\"pvx-badge\">" + dxGiftLbl + "</span></span></span>";
5152            totalPart = "";
5153          }
5154
5155          var $dxSub = $detail.find(".product_subinfo").first();
5156          var variantText = "";
5157          if ($dxSub.length) {
5158            // cor/tamanho e o ultimo segmento do subinfo (formatos com Ref/CB antes de <br>)
5159            var dxSubParts = ($dxSub.html() || "").split(/<br\s*\/?>/i);
5160            variantText = $.trim($("<div></div>").html(dxSubParts[dxSubParts.length - 1]).text());
5161            if (/^Ref:/i.test(variantText) || /CB:/i.test(variantText)) variantText = "";
5162          }
5163          $detail.find("br").replaceWith(" ");
5164          $detail.find(".product_subinfo").remove();
5165          $detail.find("a").removeAttr("style");
5166          $qty.addClass("shopcart-drawer__qty-select");
5167          $remove.addClass("shopcart-drawer__remove").removeClass("fa-minus-circle").addClass("fa-trash");
5168          if ($remove.is("button") && !$remove.attr("type")) $remove.attr("type", "button");
5169
5170          // stepper como no checkout: lixo a qty=1, +/- manipulam o select escondido
5171          var dxQtyVal = parseInt($qty.val() || "1", 10) || 1;
5172          var dxQtyMax = $qty.find("option").length || 1;
5173          var stepperPart = $qty.length ? [
5174            "<span class=\"shopcart-drawer__stepper\">",
5175            "<button type=\"button\" class=\"dx-step-minus", (dxQtyVal <= 1 ? " is-remove" : ""), "\" aria-label=\"Reduzir quantidade\">
5175", (dxQtyVal <= 1 ? "<i class=\"fa fa-trash\"></i>" : "&minus;"), "</button>",
5176            "<span class=\"dx-step-qty\">", dxQtyVal, "</span>",
5177            "<button type=\"button\" class=\"dx-step-plus\" aria-label=\"Aumentar quantidade\"", (dxQtyVal >= dxQtyMax ? " disabled" : ""), ">+</button>",
5178            "</span>"
5179          ].join("") : "";
5180
5181          items.push([
5182            "<div class=\"shopcart-drawer__item\">",
5183            "  <div class=\"shopcart-drawer__thumb\">", $img.length ? $("<div></div>").append($img).html() : "", (dxQtyVal > 1 ? "<span class=\"pvx-qty-badge\">" + dxQtyVal + "</span>" : ""), "</div>",
5184            "  <div class=\"shopcart-drawer__item-main\">",
5185            "    <div class=\"shopcart-drawer__toprow\">",
5186            "      <div class=\"shopcart-drawer__item-title\">", $detail.html() || "", variantText ? "<span class=\"shopcart-drawer__item-variant\">" + variantText + "</span>" : "", "</div>",
5187            pricesPart,
5188            "    </div>",
5189            "    <div class=\"shopcart-drawer__line-foot\">", stepperPart, totalPart, "</div>",
5190            $qty.length ? "    <span class=\"shopcart-drawer__line-controls\" style=\"display:none\">" + $("<div></div>").append($qty).html() + "</span>" : "",
5191            "  </div>",
5192            "  <div class=\"shopcart-drawer__item-remove\" style=\"display:none\">", $("<div></div>").append($remove).html(), "</div>",
5193            "</div>"
5194          ].join(""));
5195        });
5196
5197        return items.join("");
5198      }
5199
5200      // nome da zona/pais efetivo escolhido, para o banner de portes gratis do drawer
5201      function drawerFreeShipZoneName() {
5202        var $opt = $("#delivery_country option:selected");
5203        if (!$opt.length || !$opt.val()) $opt = $("#country option:selected");
5204        if ($opt.length && $opt.val()) return $.trim($opt.text());
5205        if (typeof drawerCountryName === "function") { var n = $.trim(drawerCountryName() || ""); if (n) return n; }
5206        return "";
5207      }
5208
5209      function renderDrawerFreeShipping(data, labels, currency) {
5210        var $box = $("[data-cart-drawer-free-shipping]");
5211        if (!$box.length) return;
5212        var threshold = parseFloat(data.free_shipping_threshold_cambio || data.free_shipping_threshold || 0);
5213        if (!threshold || threshold <= 0) {
5214          $box.hide().empty();
5215          return;
5216        }
5217
5218        var progress = Math.max(0, Math.min(100, parseFloat(data.free_shipping_progress || 0)));
5219        var thresholdText = data.free_shipping_threshold_cambio || data.free_shipping_threshold || "0.00";
5220        var remainingText = data.free_shipping_remaining_cambio || data.free_shipping_remaining || "0.00";
5221        var qualified = String(data.free_shipping_qualified || "0") === "1" || parseFloat(data.free_shipping_remaining || 0) <= 0;
5222        if (qualified) {
5223          // ja tem portes gratis: o banner deixa de ter utilidade (a linha Portes diz "Gratis")
5224          $box.hide().empty();
5225          return;
5226        }
5227        var statusText = labels.freeShippingRemaining2.replace("%s", currency + " " + remainingText);
5228        // {N} = pais efectivo (servidor / select / prefs) — nunca hardcode PT
5229        var fsZone = (data && data.ship_country_name) ? String(data.ship_country_name) : "";
5230        if (!fsZone) fsZone = drawerFreeShipZoneName();
5231        if (!fsZone && window.CV2_PREFS_COUNTRY_NAME) fsZone = String(window.CV2_PREFS_COUNTRY_NAME);
5232        var fromLabel = String(labels.freeShippingFrom).replace("{N}", fsZone || "");
5233        fromLabel = fromLabel.replace(/\s{2,}/g, " ").replace(/\s+from\s*$/i, " from").replace(/\s+a partir de\s*$/i, " a partir de");
5234
5235        $box.html([
5236          "<div class=\"shopcart-drawer__free-shipping-inner\" style=\"padding:13px 14px;border:1px solid #eadfe5;border-radius:8px;background:#fffafa;\">",
5237          "  <div class=\"shopcart-drawer__free-shipping-line\" style=\"display:grid;grid-template-columns:22px minmax(0,1fr);gap:10px;align-items:center;color:#332c30;\">",
5238          "    <i class=\"fa fa-truck\" aria-hidden=\"true\" style=\"font-size:18px;line-height:1;color:#332c30;\"></i>",
5239          "    <span style=\"min-width:0;font-size:14px;line-height:1.25;font-weight:700;\">
5239", fromLabel, " ", currency, " ", thresholdText, "</span>",
5240          "  </div>",
5241          "  <div class=\"shopcart-drawer__free-shipping-bar\" aria-hidden=\"true\" style=\"display:block;height:7px;margin:11px 0 9px;overflow:hidden;border-radius:999px;background:#e7deda;\"><span style=\"display:block;height:100%;border-radius:999px;background:#8d1530;width:", progress, "%;transition:width 260ms ease;\"></span></div>",
5242          "  <strong class=\"shopcart-drawer__free-shipping-status\" style=\"display:block;color:#8d1530;font-size:14px;line-height:1.25;font-weight:800;text-align:right;\">", statusText, "</strong>",
5243          "</div>"
5244        ].join("")).show();
5245      }
5246
5247      function parseDrawerMoney(text) {
5248        var value = String(text || "").replace(/[^0-9,.-]/g, "");
5249        if (value.indexOf(",") > -1 && value.indexOf(".") > -1) {
5250          value = value.replace(/\./g, "").replace(",", ".");
5251        } else {
5252          value = value.replace(",", ".");
5253        }
5254        var parsed = parseFloat(value);
5255        return isNaN(parsed) ? 0 : parsed;
5256      }
5257
5258      function formatDrawerMoney(value) {
5259        return (Math.max(0, value || 0)).toFixed(2);
5260      }
5261
5262      function drawerVisibleItemsTotal() {
5263        var total = 0;
5264        currentPanel().find(".shopcart-drawer__price-total").each(function () {
5265          total += parseDrawerMoney($(this).text());
5266        });
5267        return total;
5268      }
5269
5270      // footer in-flow + CTA pinned (2026-09-17): lista nao precisa de pad de overlay
5271      function syncDrawerFooterPad() {
5272        $(".shopcart-drawer__items").each(function () {
5273          this.style.removeProperty("padding-bottom");
5274        });
5275      }
5276
5277      function drawerTotalsPlaceholder(labels) {
5278        return [
5279          "<div class=\"shopcart-drawer__totals\" aria-live=\"polite\">",
5280          "  <div><span>", labels.subtotal, "</span><strong data-cart-drawer-total=\"subtotal\">-</strong></div>",
5281          "  <div style=\"display:none\"><span>", labels.shipping, "</span><strong data-cart-drawer-total=\"shipping\">-</strong></div>",
5282          "  <div style=\"display:none\"><span data-cart-drawer-savings-label>", labels.savings, "</span><strong data-cart-drawer-total=\"savings\">-</strong></div>",
5283          "  <div class=\"shopcart-drawer__promo\">",
5284          "    <button type=\"button\" class=\"shopcart-drawer__promo-toggle\" data-dx-promo-toggle>", labels.promoQuestion, "</button>",
5285          "    <div class=\"shopcart-drawer__promo-form\" data-dx-promo-form style=\"display:none\">",
5286          "      <span class=\"dx-promo-fieldwrap\">",
5287          "        <input type=\"text\" data-dx-promo-input placeholder=\"", labels.promoPlaceholder, "\" value=\"", drawerDiscountCode, "\">",
5288          "        <button type=\"button\" class=\"dx-promo-hide\" data-dx-promo-hide aria-label=\"Fechar\">&times;</button>",
5289          "      </span>",
5290          "      <button type=\"button\" data-dx-promo-apply>", (drawerDiscountCode ? labels.promoRemove : labels.promoApply), "</button>",
5291          "    </div>",
5292          "  </div>",
5293          "  <div class=\"shopcart-drawer__total\"><span data-cart-drawer-total-label>", labels.total, "</span><strong data-cart-drawer-total=\"total\">-</strong></div>",
5294          "</div>"
5295        ].join("");
5296      }
5297
5298      // codigo de desconto aplicado no drawer (persiste entre rebuilds do footer;
5299      // mesmo contrato do checkout: validate_discount_code + discount_code no populate)
5300      var drawerDiscountCode = (window.cv2GetDiscount ? window.cv2GetDiscount() : "");
5301
5302      // pais de entrega: prefs (globo) > localStorage "Calcular" > vazio
5303      // t-cart-prefs-ship: cv2_country_id antigo (ex. 173) nao pode ganhar a escolha NL nas prefs
5304      var prefsCountryBoot = (window.CV2_PREFS_COUNTRY ? String(window.CV2_PREFS_COUNTRY) : "");
5305      var drawerCountry = (window.cv2GetCountry ? window.cv2GetCountry() : "");
5306      if (prefsCountryBoot) {
5307        if (!drawerCountry || String(drawerCountry) !== String(prefsCountryBoot)) {
5308          drawerCountry = prefsCountryBoot;
5309          if (window.cv2SetCountry) window.cv2SetCountry(prefsCountryBoot);
5310        }
5311      }
5312      var drawerCountryList = null;
5313
5314      function renderDrawerCountrySelect() {
5315        var $slot = $("[data-cart-drawer-total=shipping]");
5316        if (!$slot.length) return;
5317        function build() {
5318          var opts = ["<option value=\"\">...</option>"];
5319          for (var i = 0; i < drawerCountryList.length; i++) {
5320            var c = drawerCountryList[i];
5321            opts.push("<option value=\"" + c.id + "\"" + (String(c.id
5321) === String(drawerCountry) ? " selected" : "") + ">" + drawerIsoFlag(c.iso) + c.nome + "</option>");
5322          }
5323          $slot.removeAttr("data-dx-ship-calc").css("cursor", "").html("<select class=\"shopcart-drawer__ship-select\" data-dx-ship-select>" + opts.join("") + "</select>");
5324        }
5325        if (drawerCountryList) { build(); return; }
5326        $.post(REQUEST_ENDPOINT, { ajax: "dx_country_list" }).done(function (resp) {
5327          try { drawerCountryList = (typeof resp === "object") ? resp : JSON.parse(resp); } catch (e) { drawerCountryList = null; }
5328          if (drawerCountryList && drawerCountryList.length) build();
5329        });
5330      }
5331
5332      $(document)
5333        .off("click.dxShipCalc")
5334        .on("click.dxShipCalc", "[data-dx-ship-calc]", function () {
5335          renderDrawerCountrySelect();
5336        });
5337
5338      $(document)
5339        .off("change.dxShipSelect")
5340        .on("change.dxShipSelect", "[data-dx-ship-select]", function () {
5341          var v = $(this).val();
5342          if (!v) return;
5343          drawerCountry = v;
5344          if (window.cv2SetCountry) window.cv2SetCountry(v);
5345          refreshDrawerTotals();
5346        });
5347
5348      // ISO 3166-1 alpha-2 -> emoji de bandeira (regional indicators; ES5 via surrogate pairs)
5349      function drawerIsoFlag(iso) {
5350        if (!iso || !/^[A-Za-z]{2}
5350$/.test(iso)) return "";
5351        function rp(ch) {
5352          var n = 0x1F1E6 + ch.toUpperCase().charCodeAt(0) - 65 - 0x10000;
5353          return String.fromCharCode(0xD800 + (n >> 10), 0xDC00 + (n & 0x3FF));
5354        }
5355        return rp(iso.charAt(0)) + rp(iso.charAt(1)) + " ";
5356      }
5357
5358      function drawerCountryName() {
5359        if (!drawerCountryList) return "";
5360        for (var i = 0; i < drawerCountryList.length; i++) {
5361          if (String(drawerCountryList[i].id) === String(drawerCountry)) return drawerIsoFlag(drawerCountryList[i].iso) + drawerCountryList[i].nome;
5362        }
5363        return "";
5364      }
5365
5366      $(document)
5367        .off("click.dxPromoClear")
5368        .on("click.dxPromoClear", "[data-dx-promo-clear]", function (e) {
5369          e.stopPropagation();
5370          drawerDiscountCode = "";
5371          if (window.cv2SetDiscount) window.cv2SetDiscount("");
5372          $("[data-dx-promo-input]").val("");
5373          $("[data-dx-promo-apply]").text(drawerLabels().promoApply);
5374          try { $.post("/cv2-discount.php", { action: "remove" }); } catch (eC) {}
5375          refreshDrawerTotals();
5376          refreshDrawerFromServer();
5377        });
5378
5379      $(document)
5380        .off("click.dxShipClear")
5381        .on("click.dxShipClear", "[data-dx-ship-clear]", function (e) {
5382          e.stopPropagation();
5383          drawerCountry = "";
5384          if (window.cv2SetCountry) window.cv2SetCountry("");
5385          refreshDrawerTotals();
5386        });
5387
5388      $(document)
5389        .off("click.dxStepPlus")
5390        .on("click.dxStepPlus", ".shopcart-drawer__item .dx-step-plus", function (e) {
5391          if (e) { e.preventDefault(); e.stopPropagation(); }
5392          var $sel = $(this).closest(".shopcart-drawer__item").find(".shopcart-drawer__qty-select").first();
5393          var v = parseInt($sel.val(), 10) || 1;
5394          if (v < ($sel.find("option").length || 1)) $sel.val(String(v + 1)).trigger("change");
5395        });
5396
5397      $(document)
5398        .off("click.dxStepMinus")
5399        .on("click.dxStepMinus", ".shopcart-drawer__item .dx-step-minus", function (e) {
5400          if (e) { e.preventDefault(); e.stopPropagation(); }
5401          var $item = $(this).closest(".shopcart-drawer__item");
5402          var $sel = $item.find(".shopcart-drawer__qty-select").first();
5403          var v = parseInt($sel.val(), 10) || 1;
5404          if (v <= 1) $item.find(".shopcart-drawer__remove").first().trigger("click");
5405          else $sel.val(String(v - 1)).trigger("change");
5406        });
5407
5408      $(document)
5409        .off("click.dxPromoToggle")
5410        .on("click.dxPromoToggle", "[data-dx-promo-toggle]", function () {
5411          // escopo ao bloco clicado: drawer e checkout partilham esta mecanica
5412          $(this).hide();
5413          var $f = $(this).closest(".shopcart-drawer__promo").find("[data-dx-promo-form]");
5414          if (!$f.length) $f = $("[data-dx-promo-form]");
5415          $f.css("display", "flex");
5416          $f.find("[data-dx-promo-input]").trigger("focus");
5417        });
5418
5419      $(document)
5420        .off("click.dxPromoHide")
5421        .on("click.dxPromoHide", "[data-dx-promo-hide]", function () {
5422          var $p = $(this).closest(".shopcart-drawer__promo");
5423          if ($p.length) { $p.find("[data-dx-promo-form]").hide(); $p.find("[data-dx-promo-toggle]").show(); }
5424          else { $("[data-dx-promo-form]").hide(); $("[data-dx-promo-toggle]").show(); }
5425        });
5426
5427      $(document)
5428        .off("click.dxPromoApply")
5429        .on("click.dxPromoApply", "[data-dx-promo-apply]", function () {
5430          var $btn = $(this);
5431          var labels = drawerLabels();
5432          if (drawerDiscountCode) {
5433            drawerDiscountCode = "";
5434            if (window.cv2SetDiscount) window.cv2SetDiscount("");
5435            $("[data-dx-promo-input]").val("");
5436            $btn.text(labels.promoApply);
5437            try { $.post("/cv2-discount.php", { action: "remove" }); } catch (eR) {}
5438            refreshDrawerTotals();
5439            refreshDrawerFromServer();
5440            return;
5441          }
5442          var $scopeInput = $btn.closest(".shopcart-drawer__promo").find("[data-dx-promo-input]");
5443          if (!$scopeInput.length) $scopeInput = $("[data-dx-promo-input]");
5444          var code = $.trim($scopeInput.val() || "");
5445          if (!code) {
5446            if (typeof toastr !== "undefined") toastr.warning("Por favor introduza um código de desconto.");
5447            return;
5448          }
5449          var emailGuess = ($("#email").val() || $("#sc_email").val() || "").toString();
5450          function cv2deLegacyApply(code) {
5451            $.ajax({ type: "POST", url: REQUEST_ENDPOINT, cache: false, data: { ajax: "validate_discount_code", discount_code: code } }).done(function (resp) {
5452              var clean = (resp || "").toString().trim();
5453              if (!isNaN(clean) && parseFloat(clean) > 0) {
5454                drawerDiscountCode = code;
5455                if (window.cv2SetDiscount) window.cv2SetDiscount(code);
5456                $btn.text(labels.promoRemove);
5457                if (typeof toastr !== "undefined") toastr.success("Desconto aplicad
5457o com sucesso.");
5458                refreshDrawerTotals();
5459                refreshDrawerFromServer();
5460              } else {
5461                $("[data-dx-promo-input]").val("");
5462                if (typeof toastr !== "undefined") toastr.error("Código de desconto inválido.");
5463              }
5464            }).fail(function () {
5465              if (typeof toastr !== "undefined") toastr.error("Não foi possível validar o código de desconto.");
5466            });
5467          }
5468          $.ajax({ type: "POST", url: "/cv2-discount.php", cache: false, data: (function(){ var d={ action: "apply", code: code }; if (emailGuess) d.email = emailGuess; return d; })() }).done(function (resp) {
5469            var r = resp;
5470            if (typeof r === "string") { try { r = JSON.parse(r); } catch (e0) { r = null; } }
5471            if (r && r.reason === "fallback") { cv2deLegacyApply(code); return; }
5472            if (r && r.ok) {
5473              drawerDiscountCode = code;
5474              if (window.cv2SetDiscount) window.cv2SetDiscount(code);
5475              $btn.text(labels.promoRemove);
5476              if (typeof toastr !== "undefined") toastr.success(r.message || "Desconto aplicado com sucesso.");
5477              refreshDrawerTotals();
5478              refreshDrawerFromServer();
5479            } else {
5480              $("[data-dx-promo-input]").val("");
5481              var msg = (r && r.message) ? r.message : "Código de desconto inválido.";
5482              if (typeof toastr !== "undefined") toastr.error(msg);
5483            }
5484          }).fail(function () { cv2deLegacyApply(code); });
5485        });
5486
5487      function refreshDrawerTotals(retryCount) {
5488        retryCount = retryCount || 0;
5489        var uid = parseInt($("#uid").val(), 10) || 0;
5490        var country = $("#country").val();
5491        var userCountry = $("#user_country").val();
5492        var prefsCountry = (window.CV2_PREFS_COUNTRY ? String(window.CV2_PREFS_COUNTRY) : "") || prefsCountryBoot || "";
5493        // t1644: ficha manda
5494        if (!country && userCountry) country = userCountry;
5495        if (country) $("#user_country").val(country);
5496        // t-cart-prefs-ship: fora do checkout, prefs/drawerCountry estimam portes (guest E login)
5497        var onCheckout = $(".checkout-modern.checkout-v2 #country").length > 0;
5498        if (!onCheckout) {
5499          if (drawerCountry) country = drawerCountry;
5500          else if (prefsCountry) country = prefsCountry;
5501        }
5502        // t1644: no checkout o formulario manda (login ou guest)
5503        if (onCheckout && $("#country").val()) {
5504          country = $("#country").val();
5505          $("#user_country").val(country);
5506        }
5507        var drawerDeliveryMethod = (typeof checkSelectedDeliveryMethod === "function") ? checkSelectedDeliveryMethod() : 0;
5508        var drawerApiPoints = (drawerDeliveryMethod === "sc_delivery_ctt" || drawerDeliveryMethod === "sc_delivery_worten") ? ($("#api_points").val() || 0) : 0;
5509
5510        $.ajax({
5511          type: "POST",
5512          url: REQUEST_ENDPOINT,
5513          data: {
5514            ajax: "populate_shopcart",
5515            uid: uid,
5516            country: country,
5517            api_points: drawerApiPoints,
5518            delivery_country: (!onCheckout && (drawerCountry || prefsCountry)) ? (drawerCountry || prefsCountry) : $("#delivery_country").val(),
5519            delivery_method: drawerDeliveryMethod,
5520            payment_method: (typeof checkSelectedPaymentMethod === "function") ? checkSelectedPaymentMethod() : 0,
5521            special_service: $("#special_service").val(),
5522            discount_code: drawerDiscountCode || (window.cv2GetDiscount ? window.cv2GetDiscount() : "") || ""
5523          }
5524        }).done(function (resp) {
5525          var data = {};
5526          try { data = (typeof resp === "object") ? resp : JSON.parse(resp); } catch (e) { data = {}; }
5527          var labels = drawerLabels();
5528          var currency = drawerCurrency();
5529          var shippingValue = parseFloat(data.shipping || 0);
5530          var discountPercent = parseFloat(data.discount_percent || 0);
5531          var savingsLabel = labels.savings + (discountPercent > 0 ? " (" + discountPercent + "%)" : "");
5532          var subtotalText = data.subtotal_nodiscount_cambio || data.subtotal_cambio || "0.00";
5533          var totalText = data.bigtotal_cambio || "0.00";
5534          var visibleItemsTotal = drawerVisibleItemsTotal();
5535          var serverSubtotal = parseDrawerMoney(subtotalText);
5536          var serverTotal = parseDrawerMoney(totalText);
5537          if (visibleItemsTotal > 0 && serverSubtotal <= 0 && serverTotal <= 0) {
5538            subtotalText = formatDrawerMoney(visibleItemsTotal);
5539            totalText = formatDrawerMoney(visibleItemsTotal);
5540            shippingValue = 0;
5541            data.free_shipping_qualified = 0;
5542            data.free_shipping_threshold = 0;
5543            data.free_shipping_threshold_cambio = 0;
5544            if (retryCount < 2) {
5545              setTimeout(function () { refreshDrawerTotals(retryCount + 1); }, 350);
5546            }
5547          }
5548
5549          // Portes definitivos apenas com morada conhecida (flag do servidor);
5550          // guest ve "A calcular" + "Total estimado" sem portes somados.
5551          var shippingKnown = String(data.shipping_known || "0") === "1";
5552          var effectiveKnown = shippingKnown || !!drawerCountry || !!prefsCountry;
5553          if (data.ship_country_id && !onCheckout && !drawerCountry && prefsCountry) {
5554            drawerCountry = String(data.ship_country_id);
5555          }
5556          $(".shopcart-drawer__footer").toggleClass("is-shipping-pending", !effectiveKnown);
5557
5558          $("[data-cart-drawer-total=subtotal]").text(currency + " " + subtotalText);
5559          $("[data-cart-drawer-total=shipping]").closest("div").show().find("[data-dx-ship-tag]").remove();
5560          if (!effectiveKnown) {
5561            // guest sem pais: CTA "Calcular" abre o select de paises de entrega
5562            $("[data-cart-drawer-total=shipping]").removeAttr("data-dx-ship-calc").css("cursor", "").html("<button type=\"button\" class=\"shopcart-drawer__ship-calc\" data-dx-ship-calc>" + labels.shippingCalc + "</button>");
5563            var estimatedTotal = Math.max(0, parseDrawerMoney(totalText) - parseFloat(data.shipping_cambio || 0));
5564            $("[data-cart-drawer-total-label]").text(labels.totalEstimated);
5565            $("[data-cart-drawer-total=total]").text(currency + " " + formatDrawerMoney(estimatedTotal) + " " + labels.plusShipping);
5566          } else {
5567            if (String(data.free_shipping_qualified || "0") === "1" || parseFloat(data.shipping_cambio || 0) === 0) {
5568              // portes a 0 mostram sempre "Gratis" (nunca "€ 0.00")
5569              $("[data-cart-drawer-total=shipping]").text(labels.shippingFree);
5570            } else {
5571              $("[data-cart-drawer-total=shipping]").text(currency + " " + (data.shipping_cambio || "0.00"));
5572            }
5573            if (!shippingKnown && drawerCountry) {
5574              // guest com pais escolhido: tag com X junto ao label; fechar volta ao "Calcular"
5575              var dxCountryName = drawerCountryName();
5576              if (dxCountryName) {
5577                $("[data-cart-drawer-total=shipping]").closest("div").find("span").first().append(
5578                  " <span class=\"shopcart-drawer__ship-tag\" data-dx-ship-tag>" + dxCountryName +
5579                  "<button type=\"button\" data-dx-ship-clear aria-label=\"" + (labels.promoRemove || gl("CHECKOUT_SUM
5579MARY_REMOVE_CODE") || "Remover") + "\">&times;</button></span>"
5580                );
5581              }
5582            }
5583            $("[data-cart-drawer-total-label]").text(labels.total);
5584            $("[data-cart-drawer-total=total]").text(currency + " " + totalText);
5585          }
5586          var discountVal = parseFloat(data.discount_cambio || data.discount || 0);
5587          var $savRow = $("[data-cart-drawer-total=savings]").closest("div");
5588          $savRow.find("[data-dx-promo-tag]").remove();
5589          if (discountVal > 0) {
5590            $savRow.show();
5591            $("[data-cart-drawer-total=savings]").text("-" + currency + " " + (data.discount_cambio || "0.00"));
5592            $("[data-cart-drawer-savings-label]").text(savingsLabel);
5593            if (drawerDiscountCode) {
5594              // codigo aplicado: tag com X junto ao label; fechar remove o desconto
5595              $("[data-cart-drawer-savings-label]").append(
5596                " <span class=\"shopcart-drawer__ship-tag\" data-dx-promo-tag>" + drawerDiscountCode +
5597                "<button type=\"button\" data-dx-promo-clear aria-label=\"" + (labels.promoRemove || gl("CHECKOUT_SUMMARY_REMOVE_CODE") || "Remover") + "\">&times;</button></span>"
5598              );
5599            }
5600          } else {
5601            $savRow.hide();
5602          }
5603          // com codigo aplicado a linha "Tem um codigo de desconto?" desaparece
5604          $(".shopcart-drawer__promo").toggle(!drawerDiscountCode);
5605          renderDrawerFreeShipping(data, labels, currency);
5606          syncDrawerFooterPad();
5607          window.setTimeout(syncDrawerFooterPad, 350);
5608        });
5609      }
5610
5611      function refreshSavedForLater() {
5612        var $slot = $currentSavedSlot();
5613        if (!$slot.length) return;
5614        if (!shopcartSavedForLaterAvailable()) {
5615          $slot.empty().hide();
5616          return;
5617        }
5618        $.post(REQUEST_ENDPOINT, { ajax: "shopcart_saved_for_later_html" }, function (response) {
5619          var data = {};
5620          try { data = (typeof response === "object") ? response : JSON.parse(response); } catch (e) { data = {}; }
5621          if (data.ok && data.html) {
5622            $slot.html(data.html).show();
5623            applySavedForLaterState($slot);
5624          } else {
5625            $slot.empty().hide();
5626          }
5627        });
5628      }
5629
5630      function refreshDrawerFromServer() {
5631        setShopcartDrawerLoading(true);
5632        // o servidor le o discount_code de $_POST (nao da sessao); sem ele, um
5633        // utilizador logado renderiza o desconto VIP em vez do codigo aplicado,
5634        // deixando os precos/badges por-produto desatualizados
5635        var code = drawerDiscountCode || (window.cv2GetDiscount ? window.cv2GetDiscount() : "") || "";
5636        $.post(REQUEST_ENDPOINT, { ajax: "generate_shopcart", type: "html", discount_code: code }).done(function (html) {
5637          injectContent(currentPanel(), html);
5638        }).always(function () {
5639          setShopcartDrawerLoading(false);
5640          refreshCheckoutAfterCartChange();
5641        });
5642      }
5643
5644      function $currentSavedSlot() {
5645        return currentPanel().find("[data-cart-saved-for-later]");
5646      }
5647
5648      function injectContent($panel, html) {
5649        if (!$panel || !$panel.length) return;
5650        var labels = drawerLabels();
5651        var $content = $("<div></div>").html(html);
5652        var itemsHtml = buildDrawerItems($content);
5653        var lang = Cookies.get("clang_id") || $("#session_lang").val() || "pt";
5654        var footerHtml = [
5655          "<div class=\"shopcart-drawer__free-shipping\" data-cart-drawer-free-shipping style=\"display:none\"></div>",
5656          drawerTotalsPlaceholder(labels),
5657          "<div class=\"cv2-dx\" id=\"cv2_dx\">",
5658          "  <div class=\"cv2-dx__title\">", labels.expressTitle, "</div>",
5659          "  <div class=\"cv2-dx__note\">", labels.expressNote, "</div>",
5660          "  <div class=\"cv2-dx__row\">",
5661          "    <div class=\"cv2-dx__slot\" id=\"cv2_dx_paypal_slot\" style=\"display:none\"></div>",
5662          "    <button type=\"button\" class=\"cv2-dx__btn cv2-dx__btn--paypal\" id=\"cv2_dx_paypal\" aria-label=\"PayPal\"><span class=\"cv2-pp-word\"><b>Pay</b>Pal</span></button>",
5663          "    <button type=\"button\" class=\"cv2-dx__btn cv2-dx__btn--gpay is-soon\" id=\"cv2_dx_gpay\" aria-label=\"Google Pay\" disabled><svg xmlns='http://www.w3.org/2000/svg' fill='none' preserveAspectRatio='xMidYMid' viewBox='0 0 126 50' focusable='false' class='pvx-gpay-svg'><g fill-rule='evenodd' clip-rule='evenodd'><path fill='#fff' d='M59.62 7.342v12.636h7.793q2.785 0 4.605-1.872c1.247-1.244 1.872-2.73 1.872-4.447q0-2.525-1.872-4.401-1.82-1.918-4.605-1.919H59.62zm0 17.083v14.657h-4.654V2.895h12.348q4.7 0 7.99 3.132 3.343 3.133 3.343 7.632c0 3.066-1.115 5.629-3.343 7.68-2.157 2.059-4.824 3.083-7.993 3.083h-7.69zm23.73 7.078q0 1.818 1.545 3.033c1.027.804 2.234 1.21 3.615 1.21 1.959 0 3.7-.724 5.236-2.17 1.538-1.451 2.302-3.153 2.302-5.106q-2.176-1.718-6.072-1.718c-1.887 0-3.466.458-4.728 1.368-1.266.91-1.897 2.034-1.897 3.383Zm6.023-17.996q5.16.002 8.148 2.754 2.982 2.758 2.984 7.558v15.263h-4.454v-3.435h-.2q-2.887 4.244-7.694 4.246-4.096-.002-6.855-2.427-2.757-2.427-2.758-6.065 0-3.843 2.91-6.116 2.909-2.274 7.767-2.274c2.76 0 5.042.504 6.827 1.516v-1.064q0-2.426-1.922-4.116a6.6 6.6 0 0 0-4.502-1.693c-2.6 0-4.655 1.092-6.174 3.287l-4.098-2.581q3.392-4.852 10.02-4.853m36.683.81L110.519 50h-4.806l5.769-12.484-10.219-23.198h5.06l7.387 17.791h.099l7.186-17.794z'/><path fill='#4285F4' d='M40.81 21.267c0-1.464-.123-2.878-.359-4.228H20.818v8.01h11.247a9.62 9.62 0 0 1-4.16 6.319v5.199h6.713c3.93-3.62 6.192-8.975 6.192-15.3'/><path fill='#34A853' d='M20.819 41.585c5.62 0 10.348-1.841 13.799-5.016l-6.713-5.203c-1.866 1.257-4.27 1.99-7.087 1.99-5.428 0-10.039-3.658-11.685-8.584H2.219v5.357a20.82 20.82 0 0 0 18.6 11.46'/><path fill='#FABB05' d='M9.133 24.771a12.5 12.5 0 0 1 0-7.96v-5.356H2.219A20.7 20.7 0 0 0 0 20.788
5663c0 3.358.805 6.53 2.219 9.337l6.914-5.357z'/><path fill='#E94235' d='M20.818 8.229c3.07 0 5.818 1.052 7.985 3.12v.002l5.942-5.935C31.142 2.06 26.438 0 20.82 0A20.82 20.82 0 0 0 2.222 11.457l6.914 5.357c1.646-4.927 6.257-8.585 11.685-8.585'/></g></svg></button>",
5664          "    <button type=\"button\" class=\"cv2-dx__btn cv2-dx__btn--apple is-soon\" id=\"cv2_dx_apple\" aria-label=\"Apple Pay\" disabled>&#63743; Pay</button>",
5665          "  </div>",
5666          "  <div class=\"cv2-dx__or\"><span>", labels.expressOr, "</span></div>",
5667          "</div>",
5668          "<button type=\"button\" class=\"shopcart-drawer__checkout\">", labels.checkout, "</button>"
5669        ].join("");
5670
5671        var isEmpty = !itemsHtml || $content.find(".shopcart_empty, .shopcart-drawer__empty").length > 0;
5672        var wrappedHtml = [
5673          "<div class=\"shopcart-drawer__head\">",
5674          "  <div>",
5675          "    <strong>", labels.title, "</strong>",
5676          "  </div>",
5677          "  <button type=\"button\" class=\"shopcart-drawer__close\" aria-label=\"", labels.close, "\"><i class=\"fa fa-times\"></i></button>",
5678          "</div>",
5679          "<div class=\"shopcart-drawer__body\">",
5680          "  <div class=\"shopcart-drawer__items\">", itemsHtml, "<div class=\"shopcart-drawer__saved-slot\" data-cart-saved-for-later></div></div>",
5681          (!isEmpty && itemsHtml) ? "<div class=\"shopcart-drawer__footer cv2-has-items\">" + footerHtml + "</div>" : "",
5682          "</div>"
5683        ].join("");
5684        var $modal = $panel.find("#shopcart-modal");
5685        if ($modal.length) {
5686          $modal.html(wrappedHtml);
5687        } else {
5688          $panel.html(wrappedHtml);
5689        }
5690        $panel.toggleClass("is-empty", isEmpty);
5691        if ($modal.length) $modal.toggleClass("is-empty", isEmpty);
5692        $panel.find(".shopcart-drawer__checkout").toggle(!isEmpty).prop("disabled", isEmpty);
5693        if (isEmpty) {
5694          $panel.find(".shopcart-drawer__checkout").attr("aria-disabled", "true").hide();
5695          $panel.find(".shopcart-drawer__footer").removeClass("cv2-has-items").hide();
5696        }
5697        if (itemsHtml && !isEmpty) refreshDrawerTotals();
5698        refreshSavedForLater();
5699      }
5700
5701      function openMini() {
5702        var $panel = currentPanel();
5703        if (!$panel.length) return;
5704        if (!$panel.parent().is("body")) $panel.appendTo("body");
5705        $("body").addClass("shopcart-drawer-open");
5706        // keep drawer rendered so CSS transform can animate
5707        setShopcartDrawerLoading(true, $panel);
5708        $.ajax({
5709          type: "POST",
5710          url: REQUEST_ENDPOINT,
5711          data: { ajax: "generate_shopcart", type: "html" }
5712        }).done(function (html) {
5713          injectContent($panel, html);
5714        }).always(function () {
5715          setShopcartDrawerLoading(false, $panel);
5716        });
5717      }
5718
5719      function closeMini() {
5720        setShopcartDrawerLoading(true);
5721        setTimeout(function () {
5722          $("body").removeClass("shopcart-drawer-open");
5723          setShopcartDrawerLoading(false);
5724        }, 160);
5725      }
5726
5727      window.openShopcartDrawer = openMini;
5728      window.closeShopcartDrawer = closeMini;
5729      window.renderShopcartDrawerHtml = function (html) {
5730        injectContent(currentPanel(), html);
5731      };
5732
5733      $(document)
5734        .off("click.shopcartDrawer keyup.shopcartDrawer")
5735        .on("click.shopcartDrawer", ".shopcart-drawer__close, #shopcart-drawer-overlay", function (e) {
5736          e.preventDefault();
5737          // X roda e desvanece antes do drawer fechar
5738          var $x = $(this).hasClass("shopcart-drawer__close") ? $(this) : $(".shopcart-drawer__close");
5739          $x.addClass("is-closing");
5740          window.setTimeout(function () {
5741            closeMini();
5742            window.setTimeout(function () { $x.removeClass("is-closing"); }, 400);
5743          }, 240);
5744        })
5745        .on("click.shopcartDrawerCheckout", ".shopcart-drawer__checkout", function (e) {
5746          e.preventDefault();
5747          if ($(this).prop("disabled") || $(this).closest(".is-empty").length) return;
5748          if (currentPanel().find(".shopcart_empty, .shopcart-drawer__empty").length && !currentPanel().find(".shopcart-drawer__item").length) return;
5749          var lang = Cookies.get("clang_id") || $("#session_lang").val() || "pt";
5750          window.location.assign("/" + lang + "/checkout/");
5751        })
5752        .on("click.shopcartEmptyCta", "[data-empty-cta]", function (e) {
5753          e.preventDefault();
5754          var href = this.getAttribute("href") || ("/" + (Cookie
5754s.get("clang_id") || $("#session_lang").val() || "pt") + "/novidades/");
5755          closeMini();
5756          window.setTimeout(function () { window.location.assign(href); }, 220);
5757        })
5758        .on("click.shopcartSavedToggle", "[data-saved-toggle]", function (e) {
5759          e.preventDefault();
5760          var $section = $(this).closest(".shopcart-drawer__saved-section");
5761          $section.toggleClass("is-collapsed");
5762          setSavedForLaterCollapsed($section.hasClass("is-collapsed"));
5763        })
5764        .on("click.shopcartSavedAdd", "[data-saved-add-to-cart]", function (e) {
5765          e.preventDefault();
5766          var $row = $(this).closest(".shopcart-drawer__saved-item");
5767          var $section = $(this).closest(".shopcart-drawer__saved-section");
5768          setShopcartDrawerLoading(true);
5769          $.post(REQUEST_ENDPOINT, {
5770            ajax: "shopcart_saved_for_later_add_to_cart",
5771            id_lista: $section.data("saved-list-id") || 0,
5772            id_code: $row.data("product-id") || 0,
5773            id_produto_elemento: $row.data("element-id") || 0,
5774            qtd: parseInt($row.find("[data-saved-qty]").first().val() || "1", 10) || 1
5775          }).always(refreshDrawerFromServer);
5776        })
5777        .on("click.shopcartSavedAddAll", "[data-saved-add-all]", function (e) {
5778          e.preventDefault();
5779          var $section = $(this).closest(".shopcart-drawer__saved-section");
5780          var listId = $section.data("saved-list-id") || 0;
5781          setShopcartDrawerLoading(true);
5782          $.post(REQUEST_ENDPOINT, { ajax: "shopcart_saved_for_later_add_all_to_cart", id_lista: listId }).always(refreshDrawerFromServer);
5783        })
5784        .on("click.shopcartSavedRemove", "[data-saved-remove]", function (e) {
5785          e.preventDefault();
5786          var $row = $(this).closest(".shopcart-drawer__saved-item");
5787          var $section = $(this).closest(".shopcart-drawer__saved-section");
5788          setShopcartDrawerLoading(true);
5789          $.post(REQUEST_ENDPOINT, {
5790            ajax: "shopcart_saved_for_later_remove",
5791            id_lista: $section.data("saved-list-id") || 0,
5792            id_code: $row.data("product-id") || 0,
5793            id_produto_elemento: $row.data("element-id") || 0
5794          }).always(refreshDrawerFromServer);
5795        })
5796        .on("keyup.shopcartDrawer", function (e) {
5797          if (e.keyCode === 27) closeMini();
5798        });
5799    }
5800
5801		// expõe global, caso outros scripts legados usem
5802		window.check_shopcart = check_shopcart;
5803		// exportado: o promo partilhado com o drawer (checkout-modern) refresca o
5804		// resumo do checkout apos aplicar/remover codigo; o afterLogin do OTP ja
5805		// testava window.populate_shopcart sem o export existir
5806		window.populate_shopcart = populate_shopcart;
5807		window.generate_shopcart = generate_shopcart;
5808		window.refreshCheckoutAfterCartChange = refreshCheckoutAfterCartChange;
5809
5810  /* ==========================================================
5811   * 11) TRACKING UTM
5812   * ========================================================== */
5813  (function utmOnce() {
5814    if (sessionStorage.getItem('utm_source')) {
5815      console.log('Source already tracked:', sessionStorage.getItem('utm_source'), sessionStorage.getItem('utm_medium'), sessionStorage.getItem('utm_campaign'));
5816      return;
5817    }
5818    function getQueryParams() { return Object.fromEntries(new URLSearchParams(window.location.search)); }
5819    var p = getQueryParams();
5820    var gclid = p.gclid, explicitSrc = p.utm_source, explicitMed = p.utm_medium, explicitCam = p.utm_campaign;
5821    var ref = document.referrer, landing = window.location.pathname;
5822    var source = explicitSrc, medium = explicitMed, campaign = explicitCam;
5823
5824    if (!source && gclid) { source = 'google'; medium = 'cpc'; }
5825    if (!source && ref) {
5826      var host = new URL(ref).hostname.replace(/^www\./, '');
5827      var socials = ['facebook.com','instagram.com','twitter.com','linkedin.com'];
5828      if (socials.some(d => host.endsWith(d))) { source = host.split('.')[0]; medium = 'social'; }
5829      else if (/(google\.[a-z]+|bing\.com|yahoo\.)/.test(host)) { source = host.includes('google.') ? 'google' : host.split('.')[0]; medium = 'organic'; }
5830      else if (/(mail\.google\.com|outlook\.live\.com|mail\.yahoo\.com)/.test(host)) { source = 'newsletter'; medium = 'email'; }
5831      else { source = host; medium = 'referral'; }
5832    }
5833    if (!source) { source = 'direct'; medium = 'none'; }
5834
5835    sessionStorage.setItem('utm_source', source);
5836    sessionStorage.setItem('utm_medium', medium);
5837    sessionStorage.setItem('utm_campaign', campaign || '');
5838    sessionStorage.setItem('referrer', ref);
5839    sessionStorage.setItem('landingPage', landing);
5840    console.log('Tracked source:', source, medium, campaign);
5841  })();
5842
5843  // ==== FAVORITES MENU (global, compat com master antigo) ====
5844  window.favoritesMenu = function () {
5845    $.ajax({
5846      type: "POST",
5847      url: REQUEST_ENDPOINT,
5848      cache: false,
5849      async: true,
5850      data: { ajax: "favoritesMenu" },
5851      success: function (html) {
5852        // todos os alvos possíveis
5853        $(".favorites-menu, #favorites-menu, .divfavorites-menu").html(html);
5854
5855        // se tiver lazy carregado
5856        if ($.fn.lazy) {
5857          $(".lazy").Lazy();
5858        }
5859      },
5860      error: function (xhr) {
5861        console.warn("favoritesMenu() failed:", xhr.status);
5862      }
5863    });
5864  };
5865
5866  if ($(".favorites-menu, #favorites-menu, .divfavorites-menu").length) {
5867    window.favoritesMenu();
5868  }
5869
5870  // ==== bloco de favoritos no carrinho (/cart/) ====
5871  window.favoritesSHreminder = function () {
5872    $.ajax({
5873      type: "POST",
5874      url: REQUEST_ENDPOINT,
5875      cache: false,
5876      data: { ajax: "favoritesSHreminder" },
5877      success: function (html) {
5878        var $slot = $("#favoritesSHreminder, .favoritesSHreminder");
5879        var clean = (html || "").trim();
5880
5881        if (!clean) {
5882          // nada para mostrar → esconde
5883          $slot.empty().hide();
5884          return;
5885        }
5886
5887        $slot.html(clean).show();
5888
5889        if ($.fn.lazy) {
5890          $(".lazy").lazy({
5891            bind: "event",
5892            effect: "fadeIn",
5893            effectTime: 500,
5894            threshold: 0
5895          });
5896        }
5897      }
5898    });
5899  };
5900
5901  // Reload/navigate: topo. Back/forward: não forçar 0 (cv2-scroll-restore.js).
5902  function resetScrollOnReload() {
5903    if (!hasWindow) return;
5904    var isBack = false;
5905    try {
5906      var nav = performance.getEntriesByType && performance.getEntriesByType("navigation")[0];
5907      if (nav && nav.type === "back_forward") isBack = true;
5908      else if (performance.navigation && performance.navigation.type === 2) isBack = true;
5909    } catch (eNav) {}
5910    if (window.history && "scrollRestoration" in window.history) {
5911      try { window.history.scrollRestoration = "manual"; } catch (e) {}
5912    }
5913    if (isBack) return;
5914    window.scrollTo(0, 0);
5915    $(window).scrollTop(0);
5916  }
5917
5918/* ===== BOOT ===== */
5919$(function () {
5920  resetScrollOnReload();
5921  ensureLangMeta();
5922  // CV2: bootstrap do token CSRF (mesma origem; usado por login/OTP/password)
5923  window.cv2Csrf = window.cv2Csrf || "";
5924  $.post(REQUEST_ENDPOINT, { ajax: "cv2_csrf" }, function (r) {
5925    try { var j = typeof r === "object" ? r : JSON.parse(r); if (j && j.csrf) window.cv2Csrf = j.csrf; } catch (e) {}
5926  });
5927  fetchLangDict().always(function(){
5928    setFiltersActiveState(false);
5929    initState();
5930    var currentCategory = getFilterCategoryKey();
5931    var savedCategory   = getSavedFilterCategory();
5932    var snapshot        = loadFiltersSnapshot(currentCategory);
5933    var urlFiltersState = currentCategory ? readFiltersStateFromUrl() : null;
5934    var categoryChanged = currentCategory && savedCategory && savedCategory !== currentCategory;
5935    fpLog("boot", {
5936      currentCategory: currentCategory,
5937      savedCategory: savedCategory,
5938      categoryChanged: categoryChanged,
5939      hasSnapshot: snapshot ? true : false,
5940      hasUrlFilters: urlFiltersState ? true : false
5941    });
5942    shouldResetFiltersOnBoot = !!categoryChanged;
5943    if (currentCategory && !savedCategory) {
5944      rememberFilterCategory(currentCategory);
5945    }
5946    bindEvents();
5947    bindMiniCartHover();
5948    check_shopcart();
5949
5950    var hasSnapshot = snapshot && hasSessionFilters(snapshot);
5951    var shouldRestoreSnapshot = false;
5952
5953    if (urlFiltersState) {
5954      postJSON("restoreFilters", { value: urlFiltersState }, function () {
5955        lastSessionFilters = urlFiltersState;
5956        rememberFilterCategory(currentCategory);
5957        syncThenRender({ resetAll: false });
5958      });
5959    } else if (shouldRestoreSnapshot) {
5960      var tasks = buildFilterTasksFromSnapshot(currentCategory, snapshot);
5961      runTasksSequentially(tasks, function () {
5962        rememberFilterCategory(currentCategory);
5963        syncThenRender({ resetAll: false });
5964      });
5965    } else {
5966      syncThenRender({ resetAll: shouldResetFiltersOnBoot });
5967    }
5968
5969    if ($("#shopcart_html").length) {
5970      generate_shopcart("html");
5971    }
5972
5973    if ($("#favoritesSHreminder").length || $(".favoritesSHreminder").length) {
5974      window.favoritesSHreminder();
5975    }
5976
5977    initAccountSection();
5978
5979  });
5980});
5981
5982})(window.jQuery);
5983
5984  function toggleLeftMenu() {
5985    var $fullWrapper = $("#full-wrapper");
5986    var $mobileMenu = $("#webstore-mobile-menu");
5987
5988    if (!$fullWrapper.hasClass("full-wrapper-open")) {
5989      $mobileMenu.addClass("webstore-mobile-menu-open").show();
5990    }
5991
5992    $("#q").val("");
5993    $("#livesearch-reset, #livesearch").hide();
5994    $("#main-container, #topm-cats, #topm-info").removeClass("main-container-search");
5995    $("#livesearch").html("").scrollTop(0);
5996
5997    $fullWrapper.toggleClass("full-wrapper-open");
5998
5999    if ($fullWrapper.hasClass("full-wrapper-open")) {
6000      $("head").append("<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>");
6001      $("#hamburguer").html("<i class='fa fa-times fa-2x mobile-menu-icon' aria-hidden='true'></i>");
6002      $("body, html").addClass("disable_hscroll");
6003      $("#main-container").addClass("main-container-search");
6004
6005      setTimeout(function () {
6006        $(".livesearch-container, .header-left-mobile-banner, .header-right-mobile-banner, #main-container, .branding").addClass("lowopacity");
6007      }, 400);
6008    } else {
6009      $(".mobile-menu-container").scrollTop(0);
6010      $("head").append("<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=yes'>");
6011      $("#hamburguer").html("<i class='fa fa-bars fa-2x mobile-menu-icon' aria-hidden='true'></i>");
6012      $("body, html").removeClass("disable_hscroll");
6013      $("#main-container").removeClass("main-container-search");
6014      $mobileMenu.hide();
6015      $(".livesearch-container, .header-left-mobile-banner, .header-right-mobile-banner, #main-container, .branding").removeClass("lowopacity");
6016    }
6017  }
6018
6019
6020$(function () {
6021
6022	// RESET PWD
6023	if ($("#reset_password").length > 0) {
6024		$("#reset_password").on("submit", function (e) {
6025			e.preventDefault();
6026			if ($("#resethash").val() && $("#resetpass").val()) {
6027				$.post("/request.php", {
6028					ajax: "redefinePWD",
6029					formhash: Base64.encode($("#resethash").val()),
6030					formpwd: Base64.encode($("#resetpass").val())
6031				},
6032					function (data, textStatus, jqXHR) {
6033						if (data == "OK") {
6034							var swalTitle = (typeof gl === "function" && gl("swal_info")) || "Informação";
6035							var swalMsg = (typeof gl === "function" && gl("swal_recoverpass_success_message")) || "Nova Password definida com sucesso!";
6036							var swalOk = (typeof gl === "function" && gl("swal_btnok")) || "OK";
6037
6038							if (typeof swal === "function") {
6039								swal({
6040									title: swalTitle,
6041									text: swalMsg,
6042									type: "success",
6043									confirmButtonColor: "#ABD559",
6044									showCancelButton: false,
6045									animation: false,
6046									confirmButtonText: swalOk,
6047									closeOnConfirm: true
6048								}, function () { window.location.href = "/"; });
6049							} else {
6050								alert(swalMsg);
6051								window.location.href = "/";
6052							}
6053						} else {
6054							alert("Não foi possível redefinir a sua nova password, por favor tente mais tarde");
6055						}
6056					},
6057					"json"
6058				);
6059			} else {
6060				console.log("error");
6061			}
6062		});
6063
6064	}
6065});

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.