PageSourceSearch

https://vestem.com/assets/storefront/js/catalog.js?v=1790834178_v38

js vestem.com collected 2026-10-09 01:14:31 UTC 76,692 bytes, 1,772 lines download raw bytes

1/**
2 * PopStore Storefront - Motor de Filtro e Streaming 100% Client-Side (/produtos)
3 */
4window.PopStoreFilterEngine = (function() {
5    let state = {
6        activeSizes: new Set(),
7        activeColors: new Set(),
8        activeTypes: new Set(),
9        activeMeta: new Set(),
10        activeBrands: new Set(),
11        activeCollections: new Set(),
12        activeDeals: new Set(),
13        priceMin: null,
14        priceMax: null,
15        boundsMin: 0,
16        boundsMax: 1000,
17        sort: 'default',
18        isStreaming: false
19    };
20
21    const SIZE_ORDER = [
22        'RN', 'PP', 'P', 'M', 'G', 'GG', 'XG', 'XXG', 'XGG', 'XXGG',
23        'G1', 'G2', 'G3', 'G4', 'G5',
24        '32', '33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '50', '52', '54', '56',
25        'U'
26    ];
27
28    const CURRENT_LANG = (window.popstore && window.popstore.lang) 
29        || (document.documentElement.lang && document.documentElement.lang.startsWith('en') ? 'en' : (document.documentElement.lang && document.documentElement.lang.startsWith('es') ? 'es' : (window.location.pathname.startsWith('/en') ? 'en' : (window.location.pathname.startsWith('/es') ? 'es' : 'br'))));
30
31    const I18N = {
32        priceRange: { br: 'Faixa de Preço', en: 'Price Range', es: 'Rango de Precios' },
33        price: { br: 'Preço', en: 'Price', es: 'Precio' },
34        minPrice: { br: 'Mín', en: 'Min', es: 'Mín' },
35        maxPrice: { br: 'Máx', en: 'Max', es: 'Máx' },
36        to: { br: 'até', en: 'to', es: 'hasta' },
37        deals: { br: 'Oportunidades & Descontos', en: 'Deals & Discounts', es: 'Ofertas y Descuentos' },
38        newIn: { br: 'Novidades', en: 'New In', es: 'Novedades' },
39        sale: { br: 'Promo', en: 'Sale', es: 'Promo' },
40        outlet: { br: 'Outlet', en: 'Outlet', es: 'Outlet' },
41        sizes: { br: 'Tamanhos', en: 'Sizes', es: 'Tallas' },
42        colors: { br: 'Cores', en: 'Colors', es: 'Colores' },
43        productTypes: { br: 'Tipos de Produto', en: 'Product Types', es: 'Tipos de Producto' },
44        specifications: { br: 'Especificações & Tecnologias', en: 'Specifications & Technologies', es: 'Especificaciones y Tecnologías' },
45        brands: { br: 'Marcas', en: 'Brands', es: 'Marcas' },
46        collections: { br: 'Coleções', en: 'Collections', es: 'Colecciones' },
47        items: { br: 'itens', en: 'items', es: 'artículos' },
48        size: { br: 'Tamanho', en: 'Size', es: 'Talla' },
49        sizeAbbr: { br: 'Tam', en: 'Size', es: 'Talla' },
50        colorAbbr: { br: 'Cor', en: 'Color', es: 'Color' },
51        loadingFull: { br: 'Carregando catálogo completo...', en: 'Loading full catalog...', es: 'Cargando catálogo completo...' },
52        loadingMore: { br: 'Carregando mais produtos...', en: 'Loading more products...', es: 'Cargando más productos...' },
53        of: { br: 'de', en: 'of', es: 'de' }
54    };
55
56    function tJs(key) {
57        const item = I18N[key];
58        if (!item) return key;
59        return item[CURRENT_LANG] || item['br'] || key;
60    }
61
62    const LOCALE_CODE = CURRENT_LANG === 'br' ? 'pt-BR' : (CURRENT_LANG === 'es' ? 'es-ES' : 'en-US');
63
64    function formatPrice(val) {
65        const num = parseFloat(val);
66        if (isNaN(num)) return 'R$ 0';
67        try {
68            const currency = CURRENT_LANG === 'en' ? 'USD' : (CURRENT_LANG === 'es' ? 'EUR' : 'BRL');
69            return new Intl.NumberFormat(LOCALE_CODE, { style: 'currency', currency: currency, maximumFractionDigits: 0 }).format(num);
70        } catch(e) {
71            return 'R$ ' + Math.round(num);
72        }
73    }
74
75    function isReferenceCode(str) {
76        if (!str) return true;
77        const s = String(str).trim();
78        if (!s) return true;
79        if (!isNaN(s)) return true;
80        if (/^[a-z]{1,4}[-_\s]?\d+([-_\s]?[a-z0-9]+)?$/i.test(s)) return true;
81        if (/^(cor|ref|sku|cod|codigo|color|prod)[-_\s]?\w+$/i.test(s)) return true;
82        if (/^#[0-9a-f]{3,8}$/i.test(s)) return true;
83        if (/[a-z0-9]*\d{3,}[a-z0-9]*/i.test(s) && !/[aeiou]{2,}/i.test(s)) return true;
84        return false;
85    }
86
87    function formatToneLabel(token) {
88        if (!token) return 'Cor';
89        return token.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
90    }
91
92    function canonicalColorSlug(slug) {
93        if (!slug) return '';
94        const s = String(slug).toLowerCase().trim().replace(/-/g, '_');
95        const aliases = {
96            'verde_claro': 'verde',
97            'verde_escuro': 'verde',
98            'menta': 'verde',
99            'militar': 'verde',
100            'musgo': 'verde',
101            'oliva': 'verde',
102            'esmeralda': 'verde',
103            'azul_claro': 'azul',
104            'azul_escuro': 'azul',
105            'marinho': 'azul',
106            'royal': 'azul',
107            'celeste': 'azul',
108            'turquesa': 'azul',
109            'indigo': 'azul',
110            'navy': 'azul',
111            'rosa_claro': 'rosa',
112            'pink': 'rosa',
113            'fucsia': 'rosa',
114            'magenta': 'rosa',
115            'rose': 'rosa',
116            'salmao': 'rosa',
117            'blush': 'rosa',
118            'cinza_claro': 'cinza',
119            'cinza_escuro': 'cinza',
120            'chumbo': 'cinza',
121            'grafite': 'cinza',
122            'mescla': 'cinza',
123            'mostarda': 'amarelo',
124            'gold': 'dourado',
125            'ouro': 'dourado',
126            'dourada': 'dourado',
127            'prata': 'prateado',
128            'prateada': 'prateado',
129            'silver': 'prateado',
130            'terracota': 'marrom',
131            'caramelo': 'marrom',
132            'chocolate': 'marrom',
133            'conhaque': 'marrom',
134            'coral': 'laranja',
135            'pessego': 'laranja',
136            'nude': 'bege',
137            'areia': 'bege',
138            'palha': 'bege',
139            'offwhite': 'off_white',
140            'marfim': 'off_white',
141            'cru': 'off_white',
142            'lavanda': 'lilas',
143            'bordo': 'vinho',
144            'marsala': 'vinho',
145            'burgundy': 'vinho'
146        };
147        return aliases[s] || s;
148    }
149
150    function normalizeSize(sz) {
151        if (!sz) return '';
152        let s = String(sz).trim();
153        if (s.includes('(')) {
154            s = s.split('(')[0].trim();
155        }
156        s = s.replace(/^tam(anho)?\.?\s+/i, '').trim();
157        const sUpper = s.toUpperCase();
158        const sAscii = sUpper.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
159
160        if (
161            ['U', 'UN', 'TU', 'ONE SIZE', 'U.'].includes(sUpper) ||
162            /\bunic[oa]s?\b/i.test(sAscii) ||
163            /^(tam(anho)?\.?\s*)?u\.?$/i.test(sAscii)
164        ) {
165            return 'U';
166        }
167
168        return sUpper;
169    }
170
171    function sortSizeKeys(keys) {
172        return keys.sort((a, b) => {
173            const idxA = SIZE_ORDER.indexOf(a);
174            const idxB = SIZE_ORDER.indexOf(b);
175            if (idxA !== -1 && idxB !== -1) return idxA - idxB;
176            if (idxA !== -1) return -1;
177            if (idxB !== -1) return 1;
178
179            const isNumA = !isNaN(a);
180            const isNumB = !isNaN(b);
181            if (isNumA && isNumB) return parseFloat(a) - parseFloat(b);
182            if (isNumA) return -1;
183            if (isNumB) return 1;
184
185            return a.localeCompare(b, undefined, { numeric: true });
186        });
187    }
188
189    function getAllCards() {
190        const cards = Array.from(document.querySelectorAll('#pop-catalog-grid > .pop-product-card, #products-grid-container > .pop-product-card, .products-grid-container > .pop-product-card'));
191        cards.forEach((c, idx) => {
192            if (!c.hasAttribute('data-initial-order')) {
193                c.setAttribute('data-initial-order', idx);
194            }
195        });
196        return cards;
197    }
198
199    /**
200     * Coleta todos os atributos disponíveis dos produtos em tela e constrói o UI dos filtros
201     */
202    function buildFilterData() {
203        const cards = getAllCards();
204        const sizesMap = {};
205        const colorsMap = {};
206        const typesMap = {};
207        const brandsMap = {};
208        const colsMap = {};
209        const metaFiltersMap = {};
210
211        // Processa as cores a partir dos swatches agrupados por tom e dados fiéis da loja
212        const sortedByDate = [...cards].sort((a, b) => (parseInt(b.getAttribute('data-date')) || 0) - (parseInt(a.getAttribute('data-date')) || 0));
213
214        sortedByDate.forEach(card => {
215            let processedSwatches = false;
216            try {
217                const swRaw = card.getAttribute('data-color-swatches');
218                if (swRaw) {
219                    const swList = JSON.parse(swRaw);
220                    if (swList && typeof swList === 'object') {
221                        Object.values(swList).forEach(sw => {
222                            if (!sw || !sw.token) return;
223                            const token = sw.token;
224                            let displayName = sw.name || sw.label || token;
225                            if (isReferenceCode(displayName)) {
226                                displayName = sw.label || formatToneLabel(token);
227                            }
228                            if (!colorsMap[token]) {
229                                colorsMap[token] = {
230                                    token: token,
231                                    code: token,
232                                    name: displayName,
233                                    hex: sw.hex || '#000000',
234                                    hex2: sw.hex2 || null,
235                                    is_pattern: token === 'estampado' || !!sw.is_pattern,
236                                    sort: sw.sort ?? 99,
237                                    count: 0
238                                };
239                            }
240                            colorsMap[token].count++;
241                            processedSwatches = true;
242                        });
243                    }
244                }
245            } catch (e) {}
246
247            if (!processedSwatches) {
248                // Fallback para cards legados
249                let colorCode = (card.getAttribute('data-color-code') || card.getAttribute('data-color-name') || card.getAttribute('data-color') || '').trim();
250                let colorName = (card.getAttribute('data-color-name') || card.getAttribute('data-color') || '').trim();
251                const colorHex = (card.getAttribute('data-color-hex') || '#000000').trim();
252                const colorHex2 = (card.getAttribute('data-color-hex2') || '').trim() || null;
253
254                if (isReferenceCode(colorName)) {
255                    try {
256                        const namesAttr = JSON.parse(card.getAttribute('data-color-names') || '[]');
257                        if (Array.isArray(namesAttr)) {
258                            for (const n of namesAttr) {
259                                if (n && !isReferenceCode(n)) {
260                                    colorName = n;
261                                    break;
262                                }
263                            }
264                        }
265                    } catch (e) {}
266                }
267
268                if (isReferenceCode(colorName)) {
269                    try {
270                        const tokensAttr = JSON.parse(card.getAttribute('data-color-tokens') || '[]');
271                        if (Array.isArray(tokensAttr) && tokensAttr[0]) {
272                            colorName = formatToneLabel(tokensAttr[0]);
273                            if (isReferenceCode(colorCode)) colorCode = tokensAttr[0];
274                        }
275                    } catch (e) {}
276                }
277
278                if (isReferenceCode(colorName)) {
279                    colorName = 'Outras';
280                }
281
282                if (colorCode) {
283                    if (!colorsMap[colorCode]) {
284                        colorsMap[colorCode] = { 
285                            token: colorCode,
286                            code: colorCode, 
287                            name: colorName, 
288                            hex: colorHex, 
289                            hex2: colorHex2, 
290                            is_pattern: colorCode === 'estampado' || colorName.toLowerCase().includes('estampa') || colorName.toLowerCase().includes('floral'),
291                            sort: 99, 
292                            count: 0 
293                        };
294                    }
295                    colorsMap[colorCode].count++;
296                }
297            }
298        });
299
300        let calculatedMinPrice = Infinity;
301        let calculatedMaxPrice = -Infinity;
302        const dealsData = {
303            hasNovidades: false,
304            hasPromo: false,
305            hasOutlet: false,
306            discountRangesMap: {}
307        };
308
309        cards.forEach(card => {
310            // 1. Tamanhos com estoque
311            try {
312                const sizesRaw = card.getAttribute('data-sizes');
313                if (sizesRaw) {
314                    const sizes = JSON.parse(sizesRaw);
315                    if (Array.isArray(sizes)) {
316                        sizes.forEach(sz => {
317                            const cleanSz = normalizeSize(sz);
318                            if (cleanSz) {
319                                sizesMap[cleanSz] = (sizesMap[cleanSz] || 0) + 1;
320                            }
321                        });
322                    }
323                }
324            } catch(e) {}
325
326            // 3. Tipos / Partes de Produtos (Top, Legging, Blusa, Shorts, etc.)
327            try {
328                const typesRaw = card.getAttribute('data-types');
329                if (typesRaw) {
330                    const types = JSON.parse(typesRaw);
331                    if (Array.isArray(types)) {
332                        types.forEach(t => {
333                            const cleanT = String(t).trim();
334                            if (cleanT) {
335                                typesMap[cleanT] = (typesMap[cleanT] || 0) + 1;
336                            }
337                        });
338                    }
339                } else {
340                    const t = (card.getAttribute('data-type') || '').trim();
341                    if (t) typesMap[t] = (typesMap[t] || 0) + 1;
342                }
343            } catch(e) {
344                const t = (card.getAttribute('data-type') || '').trim();
345                if (t) typesMap[t] = (typesMap[t] || 0) + 1;
346            }
347
348            // 4. Marcas
349            const brand = (card.getAttribute('data-brand') || '').trim();
350            if (brand) {
351                brandsMap[brand] = (brandsMap[brand] || 0) + 1;
352            }
353
354            // 5. Coleções
355            const col = (card.getAttribute('data-collection') || '').trim();
356            if (col) {
357                colsMap[col] = (colsMap[col] || 0) + 1;
358            }
359
360            // 6. Metadados & Especificações Técnicas do Produto (Independe de Loja/Marketplace)
361            try {
362                const metaRaw = card.getAttribute('data-metadata');
363                if (metaRaw) {
364                    let metaObj = JSON.parse(metaRaw);
365                    if (typeof metaObj === 'string') {
366                        metaObj = JSON.parse(metaObj);
367                    }
368                    if (metaObj && typeof metaObj === 'object') {
369                        if (metaObj.br || metaObj.pt) {
370                            metaObj = metaObj.br || metaObj.pt;
371                        }
372                        Object.entries(metaObj).forEach(([mKey, mVal]) => {
373                            if (mKey === 'images') return;
374                            const k = String(mKey).trim();
375                            const v = String(mVal).trim();
376                            if (k && v && typeof mVal !== 'object') {
377                                if (!metaFiltersMap[k]) metaFiltersMap[k] = {};
378                                metaFiltersMap[k][v] = (metaFiltersMap[k][v] || 0) + 1;
379                            }
380                        });
381                    }
382                }
383            } catch(e) {}
384            // Preço
385            const p = parseFloat(card.getAttribute('data-price') || '0');
386            if (p > 0) {
387                if (p < calculatedMinPrice) calculatedMinPrice = p;
388                if (p > calculatedMaxPrice) calculatedMaxPrice = p;
389            }
390
391            // Oportunidades & Descontos
392            const disc = parseInt(card.getAttribute('data-discount') || '0', 10);
393            if (disc <= 0) {
394                dealsData.hasNovidades = true;
395            } else if (disc < 50) {
396                dealsData.hasPromo = true;
397            } else {
398                dealsData.hasOutlet = true;
399            }
400            if (disc >= 10) {
401                const dStep = Math.min(70, Math.floor(disc / 10) * 10);
402                dealsData.discountRangesMap[dStep] = (dealsData.discountRangesMap[dStep] || 0) + 1;
403            }
404        });
405
406        if (calculatedMinPrice !== Infinity && calculatedMaxPrice !== -Infinity) {
407            state.boundsMin = Math.floor(calculatedMinPrice);
408            state.boundsMax = Math.ceil(calculatedMaxPrice);
409            if (state.boundsMax <= state.boundsMin) {
410                state.boundsMax = state.boundsMin + 100;
411            }
412        }
413
414        return { sizesMap, colorsMap, typesMap, metaFiltersMap, brandsMap, colsMap, dealsData };
415    }
416
417    /**
418     * Renderiza o HTML das seções de filtro (Preço -> Oportunidades & Descontos -> Tamanhos -> Cores -> Tipos -> Especificações -> Marcas -> Coleções)
419     */
420    function renderFiltersUI() {
421        const { sizesMap, colorsMap, typesMap, metaFiltersMap, brandsMap, colsMap, dealsData } = buildFilterData();
422        let html = '';
423
424        // 0. FAIXA DE PREÇO (Dual Range Slider no Topo dos Filtros)
425        const bMin = state.boundsMin;
426        const bMax = state.boundsMax;
427        const curMin = state.priceMin !== null ? Math.max(bMin, Math.min(state.priceMin, bMax)) : bMin;
428        const curMax = state.priceMax !== null ? Math.min(bMax, Math.max(state.priceMax, bMin)) : bMax;
429        const span = Math.max(1, bMax - bMin);
430        const pctMin = Math.max(0, Math.min(100, Math.round(((curMin - bMin) / span) * 100)));
431        const pctMax = Math.max(0, Math.min(100, Math.round(((curMax - bMin) / span) * 100)));
432        const leftOffset = `calc(${pctMin}% + ${10 - 0.2 * pctMin}px)`;
433        const rightOffset = `calc(${100 - pctMax}% + ${10 - 0.2 * (100 - pctMax)}px)`;
434
435        html += `
436        <div class="filter-group-block filter-group-price">
437            <h3 class="filter-group-title">
438                <span>${tJs('priceRange')}</span>
439            </h3>
440            <div class="filter-price-slider-container" data-min="${bMin}" data-max="${bMax}" data-cur-min="${curMin}" data-cur-max="${curMax}">
441                <div class="filter-price-display-row">
442                    <div class="filter-price-val-box">
443                        <span class="filter-price-label">${tJs('minPrice')}</span>
444                        <span class="filter-price-amount price-min-display">${formatPrice(curMin)}</span>
445                    </div>
446                    <div class="filter-price-separator">${tJs('to')}</div>
447                    <div class="filter-price-val-box is-right">
448                        <span class="filter-price-label">${tJs('maxPrice')}</span>
449                        <span class="filter-price-amount price-max-display">${formatPrice(curMax)}</span>
450                    </div>
451                </div>
452                <div class="filter-price-track-wrapper">
453                    <div class="filter-price-track-bg"></div>
454                    <div class="filter-price-track-fill" style="left: ${leftOffset}; right: ${rightOffset};"></div>
455                    <input type="range" class="filter-price-range range-min" min="${bMin}" max="${bMax}" step="1" value="${curMin}" aria-label="${tJs('minPrice')}" oninput="PopStoreFilterEngine.handlePriceInput(this, 'min')" onchange="PopStoreFilterEngine.applyPriceFilter()">
456                    <input type="range" class="filter-price-range range-max" min="${bMin}" max="${bMax}" step="1" value="${curMax}" aria-label="${tJs('maxPrice')}" oninput="PopStoreFilterEngine.handlePriceInput(this, 'max')" onchange="PopStoreFilterEngine.applyPriceFilter()">
457                </div>
458            </div>
459        </div>`;
460
461        // 0.5. OPORTUNIDADES & DESCONTOS (Abaixo do Slider de Preço)
462        if (dealsData) {
463            const { hasNovidades, hasPromo, hasOutlet, discountRangesMap } = dealsData;
464            const hasAnyDeal = hasNovidades || hasPromo || hasOutlet || Object.keys(discountRangesMap || {}).length > 0;
465            if (hasAnyDeal) {
466                html += `
467                <div class="filter-group-block filter-group-deals">
468                    <h3 class="filter-group-title">
469                        <span>${tJs('deals')}</span>
470                    </h3>
471                    <div class="filter-deals-pills-grid">`;
472
473                if (hasNovidades) {
474                    const isSel = state.activeDeals.has('novidades');
475                    html += `
476                    <button type="button" class="filter-deal-pill is-novidades ${isSel ? 'is-selected' : ''}" onclick="PopStoreFilterEngine.toggleDeal('novidades')" title="${tJs('newIn')}">
477                        <span>${tJs('newIn')}</span>
478                    </button>`;
479                }
480                if (hasPromo) {
481                    const isSel = state.activeDeals.has('promo');
482                    html += `
483                    <button type="button" class="filter-deal-pill is-promo ${isSel ? 'is-selected' : ''}" onclick="PopStoreFilterEngine.toggleDeal('promo')" title="${tJs('sale')}">
484                        <span>${tJs('sale')}</span>
485                    </button>`;
486                }
487                if (hasOutlet) {
488                    const isSel = state.activeDeals.has('outlet');
489                    html += `
490                    <button type="button" class="filter-deal-pill is-outlet ${isSel ? 'is-selected' : ''}" onclick="PopStoreFilterEngine.toggleDeal('outlet')" title="${tJs('outlet')}">
491                        <span>${tJs('outlet')}</span>
492                    </button>`;
493                }
494
495                if (discountRangesMap) {
496                    const dSteps = Object.keys(discountRangesMap).map(Number).sort((a, b) => a - b);
497                    dSteps.forEach(dStep => {
498                        const dKey = `d${dStep}`;
499                        const isSel = state.activeDeals.has(dKey);
500                        const dLabel = dStep >= 60 ? `${dStep}%+ OFF` : `${dStep}% OFF`;
501                        html += `
502                        <button type="button" class="filter-deal-pill ${isSel ? 'is-selected' : ''}" onclick="PopStoreFilterEngine.toggleDeal('${dKey}')" title="${dLabel}">
503                            <span class="deal-percent-badge">${dLabel}</span>
504                        </button>`;
505                    });
506                }
507
508                html += `
509                    </div>
510                </div>`;
511            }
512        }
513
514        // 1. TAMANHOS (Somente com estoque disponível na tela)
515        const sizeKeys = Object.keys(sizesMap);
516        if (sizeKeys.length > 0) {
517            sortSizeKeys(sizeKeys);
518
519            html += `
520            <div class="filter-group-block">
521                <h3 class="filter-group-title">
522                    <span>${tJs('sizes')}</span>
523                </h3>
524                <div class="filter-sizes-pills-grid">
525                    ${sizeKeys.map(sz => {
526                        const isSel = state.activeSizes.has(sz);
527                        return `
528                        <button type="button" 
529                                class="filter-size-pill ${isSel ? 'is-selected' : ''}" 
530                                onclick="PopStoreFilterEngine.toggleSize('${sz}')" 
531                                title="${tJs('size')} ${sz}">
532                            <span>${sz}</span>
533                        </button>`;
534                    }).join('')}
535                </div>
536            </div>`;
537        }
538
539        // 2. CORES (Pills com bolinha colorida fiel à loja e ordenação canônica de tons)
540        const colorCodes = Object.keys(colorsMap).sort((a, b) => {
541            const sortA = colorsMap[a].sort ?? 99;
542            const sortB = colorsMap[b].sort ?? 99;
543            if (sortA !== sortB) return sortA - sortB;
544            return (colorsMap[a].name || '').localeCompare(colorsMap[b].name || '');
545        });
546
547        if (colorCodes.length > 0) {
548            html += `
549            <div class="filter-group-block">
550                <h3 class="filter-group-title">
551                    <span>${tJs('colors')}</span>
552                </h3>
553                <div class="filter-colors-grid">
554                    ${colorCodes.map(cCode => {
555                        const c = colorsMap[cCode];
556                        const tokenOrCode = c.token || cCode;
557                        const isSel = state.activeColors.has(tokenOrCode) || state.activeColors.has(c.name);
558                        const isPattern = tokenOrCode === 'estampado' || cCode === 'estampado' || !!c.is_pattern;
559                        const cHex = c.hex || '#000000';
560                        const isLight = ['#ffffff', '#fff', '#f8fafc', '#f1f5f9', '#f8f5ee'].includes(cHex.toLowerCase());
561                        const borderStyle = isLight ? 'border: 1px solid #cbd5e1;' : '';
562                        const patternSvg = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'><rect width='24' height='24' fill='%23fff1f2'/><circle cx='6' cy='6' r='4' fill='%23f43f5e'/><circle cx='18' cy='18' r='4' fill='%230284c7'/><circle cx='18' cy='6' r='3.5' fill='%23eab308'/><circle cx='6' cy='18' r='3.5' fill='%2316a34a'/><circle cx='12' cy='12' r='3' fill='%239333ea'/></svg>";
563                        const bgStyle = isPattern
564                            ? ''
565                            : (c.hex2 
566                                ? `background: linear-gradient(135deg, ${cHex} 50%, ${c.hex2} 50%);` 
567                                : `background-color: ${cHex};`);
568                        return `
569                        <button type="button" 
570                                class="filter-color-pill ${isSel ? 'is-selected' : ''}" 
571                                onclick="PopStoreFilterEngine.toggleColor('${tokenOrCode}')" 
572                                title="${c.name}">
573                            <span class="color-swatch-dot ${isPattern ? 'is-pattern' : ''}" style="${bgStyle} ${borderStyle}"></span>
574                            <span class="color-swatch-name">${c.name}</span>
575                        </button>`;
576                    }).join('')}
577                </div>
578            </div>`;
579        }
580
581        // 3. TIPOS DE PRODUTOS (Ordem alfabética)
582        const typeKeys = Object.keys(typesMap).sort((a, b) => a.localeCompare(b, CURRENT_LANG === 'br' ? 'pt-BR' : 'en', { sensitivity: 'base' }));
583        if (typeKeys.length > 0) {
584            html += `
585            <div class="filter-group-block">
586                <h3 class="filter-group-title">
587                    <span>${tJs('productTypes')}</span>
588                </h3>
589                <ul class="filter-checkboxes-list">
590                    ${typeKeys.map(t => {
591                        const isSel = state.activeTypes.has(t);
592                        const safeT = t.replace(/'/g, "\\'");
593                        return `
594                        <li class="filter-checkbox-item">
595                            <label style="display: flex; align-items: center; cursor: pointer; min-width: 0; width: 100%; user-select: none;">
596                                <input type="checkbox" ${isSel ? 'checked' : ''} onchange="PopStoreFilterEngine.toggleType('${safeT}')">
597                                <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;">${t}</span>
598                            </label>
599                        </li>`;
600                    }).join('')}
601                </ul>
602            </div>`;
603        }
604
605        // Bloco: ESPECIFICAÇÕES & TECNOLOGIAS (Metadados do Produto - Exibido logo abaixo de Tipos de Produtos)
606        const metaKeys = Object.keys(metaFiltersMap).sort((a, b) => a.localeCompare(b, CURRENT_LANG === 'br' ? 'pt-BR' : 'en', { sensitivity: 'base' }));
607        if (metaKeys.length > 0) {
608            html += `
609            <div class="filter-group-block filter-group-specifications">
610                <h3 class="filter-group-title">
611                    <span>${tJs('specifications')}</span>
612                </h3>
613                <ul class="filter-checkboxes-list">`;
614            
615            metaKeys.forEach(mKey => {
616                const valMap = metaFiltersMap[mKey];
617                const valKeys = Object.keys(valMap).sort();
618                valKeys.forEach(mVal => {
619                    const token = `${mKey}:::${mVal}`;
620                    const isSel = state.activeMeta.has(token);
621                    const isBool = ['sim', 'yes', 'si', '1', 'true'].includes(mVal.toLowerCase());
622                    const displayLabel = isBool ? mKey : `${mKey}: ${mVal}`;
623                    const safeKey = mKey.replace(/'/g, "\\'");
624                    const safeVal = mVal.replace(/'/g, "\\'");
625                    html += `
626                    <li class="filter-checkbox-item">
627                        <label style="display: flex; align-items: center; cursor: pointer; min-width: 0; width: 100%; user-select: none;">
628                            <input type="checkbox" ${isSel ? 'checked' : ''} onchange="PopStoreFilterEngine.toggleMeta('${safeKey}', '${safeVal}')">
629                            <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;" title="${displayLabel}">${displayLabel}</span>
630                        </label>
631                    </li>`;
632                });
633            });
634
635            html += `
636                </ul>
637            </div>`;
638        }
639
640        // 4. MARCAS
641        const brandKeys = Object.keys(brandsMap).sort();
642        if (brandKeys.length > 0) {
643            html += `
644            <div class="filter-group-block">
645                <h3 class="filter-group-title">
646                    <span>${tJs('brands')}</span>
647                </h3>
648                <ul class="filter-checkboxes-list">
649                    ${brandKeys.map(b => {
650                        const isSel = state.activeBrands.has(b);
651                        const safeB = b.replace(/'/g, "\\'");
652                        return `
653                        <li class="filter-checkbox-item">
654                            <label style="display: flex; align-items: center; cursor: pointer; min-width: 0; width: 100%; user-select: none;">
655                                <input type="checkbox" ${isSel ? 'checked' : ''} onchange="PopStoreFilterEngine.toggleBrand('${safeB}')">
656                                <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;">${b}</span>
657                            </label>
658                        </li>`;
659                    }).join('')}
660                </ul>
661            </div>`;
662        }
663
664        // 5. COLEÇÕES
665        const colKeys = Object.keys(colsMap).sort();
666        if (colKeys.length > 0) {
667            html += `
668            <div class="filter-group-block">
669                <h3 class="filter-group-title">
670                    <span>${tJs('collections')}</span>
671                </h3>
672                <ul class="filter-checkboxes-list">
673                    ${colKeys.map(col => {
674                        const isSel = state.activeCollections.has(col);
675                        const safeCol = col.replace(/'/g, "\\'");
676                        return `
677                        <li class="filter-checkbox-item">
678                            <label style="display: flex; align-items: center; cursor: pointer; min-width: 0; width: 100%; user-select: none;">
679                                <input type="checkbox" ${isSel ? 'checked' : ''} onchange="PopStoreFilterEngine.toggleCollection('${safeCol}')">
680                                <span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer;">${col}</span>
681                            </label>
682                        </li>`;
683                    }).join('')}
684                </ul>
685            </div>`;
686        }
687
688        const dContainer = document.getElementById('desktop-filters-container');
689        if (dContainer) dContainer.innerHTML = html;
690
691        const mContainer = document.getElementById('mobile-filters-container');
692        if (mContainer) mContainer.innerHTML = html;
693
694        renderActiveTags();
695    }
696
697    /**
698     * Renderiza as tags de filtros ativos com botão (x) para remover individualmente
699     */
700    function renderActiveTags() {
701        const { colorsMap } = buildFilterData();
702        const tags = [];
703        state.activeSizes.forEach(s => tags.push({ label: `${tJs('sizeAbbr')}: ${s}`, action: () => toggleSize(s) }));
704        state.activeColors.forEach(cCode => {
705            let labelName = colorsMap[cCode]?.name || cCode.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
706            if (isReferenceCode(labelName)) {
707                labelName = formatToneLabel(cCode);
708            }
709            tags.push({ label: `${tJs('colorAbbr')}: ${labelName}`, action: () => toggleColor(cCode) });
710        });
711        state.activeTypes.forEach(t => tags.push({ label: t, action: () => toggleType(t) }));
712        state.activeMeta.forEach(token => {
713            const [k, ...vParts] = token.split(':::');
714            const v = vParts.join(':::');
715            const isBool = ['sim', 'yes', 'si', '1', 'true'].includes(v.toLowerCase());
716            const label = isBool ? k : `${k}: ${v}`;
717            tags.push({ label: label, action: () => toggleMeta(k, v) });
718        });
719        state.activeBrands.forEach(b => tags.push({ label: b, action: () => toggleBrand(b) }));
720        state.activeCollections.forEach(col => tags.push({ label: col, action: () => toggleCollection(col) }));
721
722        state.activeDeals.forEach(dealKey => {
723            let label = dealKey;
724            if (dealKey === 'novidades') label = tJs('newIn');
725            else if (dealKey === 'promo') label = tJs('sale');
726            else if (dealKey === 'outlet') label = tJs('outlet');
727            else if (dealKey.startsWith('d')) {
728                const s = dealKey.substring(1);
729                label = s >= 60 ? `${s}%+ OFF` : `${s}% OFF`;
730            }
731            tags.push({ label: label, action: () => toggleDeal(dealKey) });
732        });
733
734        if (state.priceMin !== null || state.priceMax !== null) {
735            const hasMin = state.priceMin !== null && state.priceMin > state.boundsMin;
736            const hasMax = state.priceMax !== null && state.priceMax < state.boundsMax;
737            if (hasMin || hasMax) {
738                const pMinDisplay = formatPrice(state.priceMin !== null ? state.priceMin : state.boundsMin);
739                const pMaxDisplay = formatPrice(state.priceMax !== null ? state.priceMax : state.boundsMax);
740                tags.push({
741                    label: `${tJs('price')}: ${pMinDisplay} - ${pMaxDisplay}`,
742                    action: () => {
743                        state.priceMin = null;
744                        state.priceMax = null;
745                        syncAllPriceSliders(state.boundsMin, state.boundsMax);
746                        apply();
747                    }
748                });
749            }
750        }
751
752        const totalActive = tags.length;
753        const btnClear = document.getElementById('btn-clear-all-filters-desktop');
754        if (btnClear) btnClear.style.display = totalActive > 0 ? 'inline' : 'none';
755
756        const badgeMobile = document.getElementById('mobile-filter-count-badge');
757        if (badgeMobile) {
758            badgeMobile.style.display = totalActive > 0 ? 'inline-flex' : 'none';
759            badgeMobile.textContent = totalActive;
760        }
761
762        const tagsHtml = tags.map((t, idx) => `
763            <span class="active-filter-tag">
764                <span>${t.label}</span>
765                <span class="tag-remove" onclick="PopStoreFilterEngine.removeTag(${idx})">&times;</span>
766            </span>
767        `).join('');
768
769        window._activeTagsActions = tags.map(t => t.action);
770
771        const dTags = document.getElementById('desktop-active-filter-tags');
772        if (dTags) dTags.innerHTML = tagsHtml;
773
774        const mTags = document.getElementById('mobile-active-filter-tags');
775        if (mTags) mTags.innerHTML = tagsHtml;
776    }
777
778    function removeTag(idx) {
779        if (window._activeTagsActions && window._activeTagsActions[idx]) {
780            window._activeTagsActions[idx]();
781        }
782    }
783
784    let priceDebounceTimer = null;
785
786    /**
787     * Manipula o input em tempo real dos sliders duplos de preço
788     */
789    function handlePriceInput(inputEl, type) {
790        const container = inputEl.closest('.filter-price-slider-container');
791        if (!container) return;
792        const rangeMin = container.querySelector('.range-min');
793        const rangeMax = container.querySelector('.range-max');
794        if (!rangeMin || !rangeMax) return;
795
796        let valMin = parseFloat(rangeMin.value);
797        let valMax = parseFloat(rangeMax.value);
798
799        if (type === 'min') {
800            rangeMin.classList.add('is-active');
801            rangeMax.classList.remove('is-active');
802            if (valMin > valMax) {
803                valMin = valMax;
804                rangeMin.value = valMin;
805            }
806        } else {
807            rangeMax.classList.add('is-active');
808            rangeMin.classList.remove('is-active');
809            if (valMax < valMin) {
810                valMax = valMin;
811                rangeMax.value = valMax;
812            }
813        }
814
815        state.priceMin = valMin;
816        state.priceMax = valMax;
817
818        syncAllPriceSliders(valMin, valMax);
819
820        clearTimeout(priceDebounceTimer);
821        priceDebounceTimer = setTimeout(() => {
822            apply(true);
823        }, 300);
824    }
825
826    function applyPriceFilter() {
827        clearTimeout(priceDebounceTimer);
828        apply(true);
829    }
830
831    function syncAllPriceSliders(valMin, valMax) {
832        const containers = document.querySelectorAll('.filter-price-slider-container');
833        containers.forEach(container => {
834            const rangeMin = container.querySelector('.range-min');
835            const rangeMax = container.querySelector('.range-max');
836            const fill = container.querySelector('.filter-price-track-fill');
837            const minDisp = container.querySelector('.price-min-display');
838            const maxDisp = container.querySelector('.price-max-display');
839
840            const bMin = parseFloat(container.getAttribute('data-min')) || state.boundsMin || 0;
841            const bMax = parseFloat(container.getAttribute('data-max')) || state.boundsMax || 1000;
842            const span = Math.max(1, bMax - bMin);
843
844            const safeMin = Math.min(valMin, valMax);
845            const safeMax = Math.max(valMin, valMax);
846
847            if (rangeMin && rangeMin !== document.activeElement) rangeMin.value = safeMin;
848            if (rangeMax && rangeMax !== document.activeElement) rangeMax.value = safeMax;
849
850            if (minDisp) minDisp.textContent = formatPrice(safeMin);
851            if (maxDisp) maxDisp.textContent = formatPrice(safeMax);
852
853            if (fill) {
854                const pctMin = Math.max(0, Math.min(100, ((safeMin - bMin) / span) * 100));
855                const pctMax = Math.max(0, Math.min(100, ((safeMax - bMin) / span) * 100));
856                if (pctMin >= pctMax) {
857                    fill.style.display = 'none';
858                } else {
859                    fill.style.display = '';
860                    fill.style.left = `calc(${pctMin}% + ${10 - 0.2 * pctMin}px)`;
861                    fill.style.right = `calc(${100 - pctMax}% + ${10 - 0.2 * (100 - pctMax)}px)`;
862                    fill.style.width = 'auto';
863                }
864            }
865        });
866    }
867
868    /**
869     * Verifica se um card corresponde aos filtros ativos de cor
870     */
871    function cardMatchesColor(card) {
872        if (state.activeColors.size === 0) return true;
873
874        const activeColorsCanon = Array.from(state.activeColors).map(c => canonicalColorSlug(c));
875
876        // 1. Verifica tokens de grupos/tons (ex: 'verde', 'azul', 'rosa')
877        try {
878            const tokensRaw = card.getAttribute('data-color-tokens');
879            if (tokensRaw) {
880                const tokens = JSON.parse(tokensRaw);
881                if (Array.isArray(tokens) && tokens.some(t => {
882                    const tCanon = canonicalColorSlug(t);
883                    return activeColorsCanon.includes(tCanon) || state.activeColors.has(tCanon) || state.activeColors.has(t);
884                })) {
885                    return true;
886                }
887            }
888        } catch (e) {}
889
890        // 2. Verifica nomes exatos das cores oficiais da loja
891        try {
892            const namesRaw = card.getAttribute('data-color-names');
893            if (namesRaw) {
894                const names = JSON.parse(namesRaw);
895                if (Array.isArray(names) && names.some(n => {
896                    const nCanon = canonicalColorSlug(n);
897                    return activeColorsCanon.includes(nCanon) || state.activeColors.has(nCanon) || state.activeColors.has(n);
898                })) {
899                    return true;
900                }
901            }
902        } catch (e) {}
903
904        // 3. Fallback para atributos diretos do card
905        const colorCode = canonicalColorSlug(card.getAttribute('data-color-code') || '');
906        const colorName = canonicalColorSlug(card.getAttribute('data-color-name') || '');
907        const color = canonicalColorSlug(card.getAttribute('data-color') || '');
908
909        if (colorCode && (activeColorsCanon.includes(colorCode) || state.activeColors.has(colorCode))) return true;
910        if (colorName && (activeColorsCanon.includes(colorName) || state.activeColors.has(colorName))) return true;
911        if (color && (activeColorsCanon.includes(color) || state.activeColors.has(color))) return true;
912
913        return false;
914    }
915
916    /**
917     * Verifica se um card corresponde aos filtros ativos de tamanho com estoque
918     */
919    function cardMatchesSize(card) {
920        if (state.activeSizes.size === 0) return true;
921        try {
922            const raw = card.getAttribute('data-sizes');
923            if (!raw) return false;
924            const sizes = JSON.parse(raw);
925            if (!Array.isArray(sizes)) return false;
926            return sizes.some(s => {
927                const clean = normalizeSize(s);
928                return clean && state.activeSizes.has(clean);
929            });
930        } catch (_) {
931            return false;
932        }
933    }
934
935    /**
936     * Verifica se um card corresponde aos filtros ativos de tipos / partes do produto
937     */
938    function cardMatchesType(card) {
939        if (state.activeTypes.size === 0) return true;
940        const activeLower = Array.from(state.activeTypes).map(x => String(x).toLowerCase().trim());
941        const t = (card.getAttribute('data-type') || '').toLowerCase().trim();
942        if (t && activeLower.includes(t)) return true;
943        try {
944            const raw = card.getAttribute('data-types');
945            if (raw) {
946                const types = JSON.parse(raw);
947                if (Array.isArray(types) && types.some(x => activeLower.includes(String(x).toLowerCase().trim()))) {
948                    return true;
949                }
950            }
951        } catch (_) {}
952        return false;
953    }
954
955    /**
956     * Verifica se um card corresponde aos filtros ativos de marca
957     */
958    function cardMatchesBrand(card) {
959        if (state.activeBrands.size === 0) return true;
960        const b = (card.getAttribute('data-brand') || '').trim();
961        return b && state.activeBrands.has(b);
962    }
963
964    /**
965     * Verifica se um card corresponde aos filtros ativos de coleção
966     */
967    function cardMatchesCollection(card) {
968        if (state.activeCollections.size === 0) return true;
969        const c = (card.getAttribute('data-collection') || '').trim();
970        return c && state.activeCollections.has(c);
971    }
972
973    /**
974     * Verifica se um card corresponde à faixa de preço ativa
975     */
976    function cardMatchesPrice(card) {
977        const p = parseFloat(card.getAttribute('data-price')) || 0;
978        if (state.priceMin !== null && p < state.priceMin) return false;
979        if (state.priceMax !== null && p > state.priceMax) return false;
980        return true;
981    }
982
983    /**
984     * Verifica se um card corresponde aos filtros de oportunidade / descontos
985     */
986    function cardMatchesDeal(card) {
987        if (state.activeDeals.size === 0) return true;
988        const disc = parseFloat(card.getAttribute('data-discount')) || 0;
989        for (const deal of state.activeDeals) {
990            if (deal === 'novidades' && disc <= 0) return true;
991            if ((deal === 'promocoes' || deal === 'promo') && disc > 0 && disc < 50) return true;
992            if (deal === 'outlet' && disc >= 50) return true;
993            if (deal.startsWith('disc_')) {
994                const minDisc = parseInt(deal.replace('disc_', ''), 10) || 0;
995                if (disc >= minDisc) return true;
996            }
997            if (/^d\d+$/.test(deal)) {
998                const step = parseInt(deal.substring(1), 10) || 0;
999                if (step >= 60) {
1000                    if (disc >= step) return true;
1001                } else {
1002                    if (disc >= step && disc < (step + 10)) return true;
1003                }
1004            }
1005        }
1006        return false;
1007    }
1008
1009    /**
1010     * Verifica se um card corresponde aos filtros ativos de metadados / especificações
1011     */
1012    function cardMatchesMeta(card) {
1013        if (state.activeMeta.size === 0) return true;
1014        try {
1015            const metaRaw = card.getAttribute('data-metadata');
1016            if (!metaRaw) return false;
1017            let metaObj = JSON.parse(metaRaw);
1018            if (typeof metaObj === 'string') {
1019                metaObj = JSON.parse(metaObj);
1020            }
1021            if (!metaObj || typeof metaObj !== 'object') return false;
1022            if (metaObj.br || metaObj.pt) {
1023                metaObj = metaObj.br || metaObj.pt;
1024            }
1025
1026            const requiredGroups = {};
1027            state.activeMeta.forEach(token => {
1028                const [k, ...vParts] = token.split(':::');
1029                const v = vParts.join(':::').toLowerCase();
1030                if (!requiredGroups[k]) requiredGroups[k] = [];
1031                requiredGroups[k].push(v);
1032            });
1033
1034            for (const [k, allowedVals] of Object.entries(requiredGroups)) {
1035                const cardVal = metaObj[k];
1036                if (cardVal === undefined || cardVal === null) return false;
1037                const strVal = String(cardVal).trim().toLowerCase();
1038                if (!allowedVals.includes(strVal)) return false;
1039            }
1040            return true;
1041        } catch (e) {
1042            return false;
1043        }
1044    }
1045
1046    /**
1047     * Sincroniza o estado atual dos filtros na URL da barra de endereços (history.replaceState)
1048     */
1049    function syncUrlParams() {
1050        try {
1051            const url = new URL(window.location.href);
1052            const params = url.searchParams;
1053
1054            // 0. Preço
1055            if (state.priceMin !== null && state.priceMin > state.boundsMin) {
1056                params.set('pmin', Math.round(state.priceMin));
1057            } else {
1058                params.delete('pmin');
1059            }
1060            if (state.priceMax !== null && state.priceMax < state.boundsMax) {
1061                params.set('pmax', Math.round(state.priceMax));
1062            } else {
1063                params.delete('pmax');
1064            }
1065
1066            // 1. Cores
1067            if (state.activeColors.size > 0) {
1068                params.set('cores', Array.from(state.activeColors).join(','));
1069            } else {
1070                params.delete('cores');
1071                params.delete('colors');
1072                params.delete('cor');
1073            }
1074
1075            // 2. Tamanhos
1076            if (state.activeSizes.size > 0) {
1077                params.set('tamanhos', Array.from(state.activeSizes).join(','));
1078            } else {
1079                params.delete('tamanhos');
1080                params.delete('sizes');
1081                params.delete('tamanho');
1082            }
1083
1084            // 3. Tipos
1085            if (state.activeTypes.size > 0) {
1086                params.set('tipos', Array.from(state.activeTypes).join(','));
1087            } else {
1088                params.delete('tipos');
1089                params.delete('types');
1090                params.delete('tipo');
1091            }
1092
1093            // Metadados / Especificações
1094            if (state.activeMeta.size > 0) {
1095                params.set('meta', Array.from(state.activeMeta).join(';;'));
1096            } else {
1097                params.delete('meta');
1098                params.delete('specs');
1099            }
1100
1101            // 4. Marcas
1102            if (state.activeBrands.size > 0) {
1103                params.set('marcas', Array.from(state.activeBrands).join(','));
1104            } else {
1105                params.delete('marcas');
1106                params.delete('brands');
1107                params.delete('marca');
1108            }
1109
1110            // 5. Coleções
1111            if (state.activeCollections.size > 0) {
1112                params.set('colecoes', Array.from(state.activeCollections).join(','));
1113            } else {
1114                params.delete('colecoes');
1115                params.delete('collections');
1116                params.delete('colecao');
1117            }
1118
1119            // Oportunidades & Descontos
1120            if (state.activeDeals.size > 0) {
1121                params.set('descontos', Array.from(state.activeDeals).join(','));
1122            } else {
1123                params.delete('descontos');
1124                params.delete('deals');
1125            }
1126
1127            // 6. Ordenação
1128            if (state.sort && state.sort !== 'default') {
1129                params.set('ordem', state.sort);
1130            } else {
1131                params.delete('ordem');
1132                params.delete('sort');
1133            }
1134
1135            const newSearch = params.toString();
1136            const newUrl = url.pathname + (newSearch ? '?' + newSearch : '') + url.hash;
1137
1138            if (window.location.pathname + window.location.search !== url.pathname + (newSearch ? '?' + newSearch : '')) {
1139                window.history.replaceState({ popstore_filters: true }, '', newUrl);
1140            }
1141        } catch (e) {
1142            console.warn('Erro ao sincronizar parâmetros da URL:', e);
1143        }
1144    }
1145
1146    /**
1147     * Carrega e ativa os filtros passados na query string da URL (para links compartilhados)
1148     */
1149    function loadStateFromUrl() {
1150        try {
1151            const params = new URLSearchParams(window.location.search);
1152            let hasChanges = false;
1153
1154            // 0. Preço
1155            const pmin = params.get('pmin');
1156            const pmax = params.get('pmax');
1157            if (pmin !== null && !isNaN(pmin)) {
1158                state.priceMin = parseFloat(pmin);
1159                hasChanges = true;
1160            } else {
1161                state.priceMin = null;
1162            }
1163            if (pmax !== null && !isNaN(pmax)) {
1164                state.priceMax = parseFloat(pmax);
1165                hasChanges = true;
1166            } else {
1167                state.priceMax = null;
1168            }
1169
1170            // Oportunidades & Descontos
1171            state.activeDeals.clear();
1172            const rawDeals = params.get('descontos') || params.get('deals');
1173            if (rawDeals) {
1174                rawDeals.split(',').forEach(d => {
1175                    const clean = d.trim();
1176                    if (clean) {
1177                        state.activeDeals.add(clean);
1178                        hasChanges = true;
1179                    }
1180                });
1181            }
1182
1183            // 1. Cores
1184            state.activeColors.clear();
1185            const rawColors = params.get('cores') || params.get('colors') || params.get('cor');
1186            if (rawColors) {
1187                rawColors.split(',').forEach(c => {
1188                    const clean = c.trim().toLowerCase();
1189                    if (clean) {
1190                        const canon = canonicalColorSlug(clean);
1191                        state.activeColors.add(canon);
1192                        hasChanges = true;
1193                    }
1194                });
1195            }
1196
1197            // 2. Tamanhos
1198            state.activeSizes.clear();
1199            const rawSizes = params.get('tamanhos') || params.get('sizes') || params.get('tamanho');
1200            if (rawSizes) {
1201                rawSizes.split(',').forEach(s => {
1202                    const clean = normalizeSize(s.trim());
1203                    if (clean) {
1204                        state.activeSizes.add(clean);
1205                        hasChanges = true;
1206                    }
1207                });
1208            }
1209
1210            // 3. Tipos
1211            state.activeTypes.clear();
1212            const rawTypes = params.get('tipos') || params.get('types') || params.get('tipo');
1213            if (rawTypes) {
1214                rawTypes.split(',').forEach(t => {
1215                    const clean = t.trim();
1216                    if (clean) {
1217                        state.activeTypes.add(clean);
1218                        hasChanges = true;
1219                    }
1220                });
1221            }
1222
1223            // Metadados / Especificações
1224            state.activeMeta.clear();
1225            const rawMeta = params.get('meta') || params.get('specs');
1226            if (rawMeta) {
1227                rawMeta.split(';;').forEach(item => {
1228                    const clean = item.trim();
1229                    if (clean && clean.includes(':::')) {
1230                        state.activeMeta.add(clean);
1231                        hasChanges = true;
1232                    }
1233                });
1234            }
1235
1236            // 4. Marcas
1237            state.activeBrands.clear();
1238            const rawBrands = params.get('marcas') || params.get('brands') || params.get('marca');
1239            if (rawBrands) {
1240                rawBrands.split(',').forEach(b => {
1241                    const clean = b.trim();
1242                    if (clean) {
1243                        state.activeBrands.add(clean);
1244                        hasChanges = true;
1245                    }
1246                });
1247            }
1248
1249            // 5. Coleções
1250            state.activeCollections.clear();
1251            const rawCols = params.get('colecoes') || params.get('collections') || params.get('colecao');
1252            if (rawCols) {
1253                rawCols.split(',').forEach(col => {
1254                    const clean = col.trim();
1255                    if (clean) {
1256                        state.activeCollections.add(clean);
1257                        hasChanges = true;
1258                    }
1259                });
1260            }
1261
1262            // 6. Ordenação
1263            const rawSort = params.get('ordem') || params.get('sort');
1264            if (rawSort) {
1265                state.sort = rawSort;
1266                const sortSelect = document.getElementById('pop-catalog-sort-select') || document.getElementById('catalog-sort-select') || document.getElementById('sort-by-select');
1267                if (sortSelect) sortSelect.value = rawSort;
1268                if (rawSort !== 'default') {
1269                    hasChanges = true;
1270                }
1271            }
1272
1273            return hasChanges;
1274        } catch (e) {
1275            console.warn('Erro ao carregar filtros da URL:', e);
1276            return false;
1277        }
1278    }
1279
1280    function syncActivePillClasses() {
1281        document.querySelectorAll('[data-filter-size]').forEach(el => {
1282            const sz = el.getAttribute('data-filter-size');
1283            const isSel = state.activeSizes.has(sz);
1284            el.classList.toggle('is-active', isSel);
1285            el.classList.toggle('is-selected', isSel);
1286        });
1287        document.querySelectorAll('[data-filter-color]').forEach(el => {
1288            const clr = el.getAttribute('data-filter-color');
1289            const isSel = state.activeColors.has(clr);
1290            el.classList.toggle('is-active', isSel);
1291            el.classList.toggle('is-selected', isSel);
1292        });
1293        document.querySelectorAll('input[data-filter-type]').forEach(el => {
1294            const t = el.getAttribute('data-filter-type');
1295            el.checked = state.activeTypes.has(t);
1296        });
1297        document.querySelectorAll('input[data-filter-brand]').forEach(el => {
1298            const b = el.getAttribute('data-filter-brand');
1299            el.checked = state.activeBrands.has(b);
1300        });
1301        document.querySelectorAll('input[data-filter-col]').forEach(el => {
1302            const col = el.getAttribute('data-filter-col');
1303            el.checked = state.activeCollections.has(col);
1304        });
1305        document.querySelectorAll('[data-deal]').forEach(el => {
1306            const deal = el.getAttribute('data-deal');
1307            const isSel = state.activeDeals.has(deal);
1308            el.classList.toggle('is-active', isSel);
1309            el.classList.toggle('is-selected', isSel);
1310        });
1311        document.querySelectorAll('input[data-filter-meta]').forEach(el => {
1312            const token = el.getAttribute('data-filter-meta');
1313            el.checked = state.activeMeta.has(token);
1314        });
1315    }
1316
1317    let filterAbortController = null;
1318
1319    /**
1320     * Aplica os filtros ativos recarregando o lote de produtos correspondentes a partir do offset 0
1321     * Garante paginação e scroll infinito consistentes em qualquer tamanho de catálogo
1322     */
1323    async function apply(updateUrl = true) {
1324        syncActivePillClasses();
1325        renderActiveTags();
1326        if (updateUrl) {
1327            syncUrlParams();
1328        }
1329
1330        const container = document.getElementById('pop-catalog-grid') || document.getElementById('products-grid-container') || document.querySelector('.products-grid-container');
1331        const loader = document.getElementById('pop-catalog-stream-loader') || document.getElementById('catalog-stream-loader');
1332        if (!container) return;
1333
1334        // Feedback visual imediato: reduz levemente opacidade durante reload
1335        container.style.transition = 'opacity 0.15s ease';
1336        container.style.opacity = '0.4';
1337        container.style.pointerEvents = 'none';
1338
1339        if (filterAbortController) {
1340            filterAbortController.abort();
1341        }
1342        filterAbortController = new AbortController();
1343
1344        try {
1345            const currentUrl = new URL(window.location.href);
1346            currentUrl.searchParams.set('partial_grid', '1');
1347            currentUrl.searchParams.delete('offset');
1348            currentUrl.searchParams.delete('limit');
1349            currentUrl.searchParams.delete('page');
1350            currentUrl.searchParams.delete('partial_batch');
1351
1352            const res = await fetch(currentUrl.toString(), {
1353                signal: filterAbortController.signal,
1354                headers: { 'X-Requested-With': 'XMLHttpRequest' }
1355            });
1356            if (!res.ok) throw new Error('HTTP ' + res.status);
1357
1358            const data = await res.json();
1359
1360            // Atualiza grid HTML
1361            container.innerHTML = data.html || '';
1362            container.style.opacity = '1';
1363            container.style.pointerEvents = 'auto';
1364
1365            // Rola a tela suavemente para o início dos produtos caso o usuário estivesse rolado para baixo
1366            const catalogTopTarget = document.getElementById('pop-catalog-header') || document.getElementById('pop-catalog-layout') || container;
1367            if (catalogTopTarget) {
1368                const currentY = window.pageYOffset || document.documentElement.scrollTop || 0;
1369                const targetY = catalogTopTarget.getBoundingClientRect().top + currentY - 16;
1370                if (currentY > targetY + 60) {
1371                    window.scrollTo({
1372                        top: Math.max(0, targetY),
1373                        behavior: 'smooth'
1374                    });
1375                }
1376            }
1377
1378            // Atualiza contadores
1379            const total = parseInt(data.totalCount) || 0;
1380            const loaded = parseInt(data.loadedCount) || 0;
1381
1382            const countBadge = document.getElementById('catalog-visible-count');
1383            if (countBadge) countBadge.textContent = total.toLocaleStr
1383ing(LOCALE_CODE);
1384
1385            const drawerCount = document.getElementById('mobile-drawer-apply-count');
1386            if (drawerCount) drawerCount.textContent = total.toLocaleString(LOCALE_CODE);
1387
1388            // Atualiza Stream Loader com novo baseUrl, offset e total
1389            if (loader) {
1390                const streamBaseUrl = new URL(window.location.pathname, window.location.origin);
1391                const currentParams = new URLSearchParams(window.location.search);
1392                currentParams.forEach((v, k) => {
1393                    if (k !== 'partial_batch' && k !== 'partial_grid' && k !== 'offset' && k !== 'limit' && k !== 'page') {
1394                        streamBaseUrl.searchParams.set(k, v);
1395                    }
1396                });
1397                streamBaseUrl.searchParams.set('partial_batch', '1');
1398
1399                loader.setAttribute('data-base-url', streamBaseUrl.pathname + '?' + streamBaseUrl.searchParams.toString());
1400                loader.setAttribute('data-offset', loaded);
1401                loader.setAttribute('data-loaded-count', loaded);
1402                loader.setAttribute('data-total-count', total);
1403
1404                const statusText = document.getElementById('stream-status-text');
1405                if (statusText) {
1406                    statusText.innerHTML = `${tJs('loadingMore')} (<b>${loaded}</b> ${tJs('of')} <b>${total}</b>)`;
1407                }
1408
1409                if (total > loaded) {
1410                    loader.style.display = '';
1411                    loader.style.opacity = '1';
1412                } else {
1413                    loader.style.display = 'none';
1414                }
1415            }
1416
1417            // Re-sincroniza componentes auxiliares
1418            if (window.PopStoreWishlist && typeof PopStoreWishlist.syncCards === 'function') {
1419                PopStoreWishlist.syncCards(container);
1420            }
1421            if (typeof initCardVideos === 'function') {
1422                initCardVideos(container);
1423            }
1424            if (typeof lucide !== 'undefined') {
1425                lucide.createIcons();
1426            }
1427
1428            setupInfiniteScroll();
1429
1430        } catch (err) {
1431            if (err.name === 'AbortError') return;
1432            console.error('Erro ao atualizar catálogo por filtro:', err);
1433            container.style.opacity = '1';
1434            container.style.pointerEvents = 'auto';
1435        }
1436    }
1437
1438    /**
1439     * Ordena os cards no DOM respeitando a opção selecionada pelo usuário
1440     */
1441    function sortCards(sortKey) {
1442        const container = document.getElementById('pop-catalog-grid') || document.getElementById('products-grid-container') || document.querySelector('.products-grid-container');
1443        if (!container) return;
1444
1445        const cards = getAllCards();
1446        cards.sort((a, b) => {
1447            if (sortKey === 'news') {
1448                const dateB = parseInt(b.getAttribute('data-date')) || parseInt(b.getAttribute('data-product-id')) || 0;
1449                const dateA = parseInt(a.getAttribute('data-date')) || parseInt(a.getAttribute('data-product-id')) || 0;
1450                if (dateB !== dateA) return dateB - dateA;
1451                return (parseInt(b.getAttribute('data-product-id')) || 0) - (parseInt(a.getAttribute('data-product-id')) || 0);
1452            }
1453            if (sortKey === 'price_asc') {
1454                const priceA = parseFloat(a.getAttribute('data-price')) || 0;
1455                const priceB = parseFloat(b.getAttribute('data-price')) || 0;
1456                if (priceA !== priceB) return priceA - priceB;
1457                return (parseInt(a.getAttribute('data-initial-order')) || 0) - (parseInt(b.getAttribute('data-initial-order')) || 0);
1458            }
1459            if (sortKey === 'price_desc') {
1460                const priceA = parseFloat(a.getAttribute('data-price')) || 0;
1461                const priceB = parseFloat(b.getAttribute('data-price')) || 0;
1462                if (priceA !== priceB) return priceB - priceA;
1463                return (parseInt(a.getAttribute('data-initial-order')) || 0) - (parseInt(b.getAttribute('data-initial-order')) || 0);
1464            }
1465            if (sortKey === 'discounts') {
1466                const discA = parseInt(a.getAttribute('data-discount')) || 0;
1467                const discB = parseInt(b.getAttribute('data-discount')) || 0;
1468                if (discB !== discA) return discB - discA;
1469                return (parseInt(a.getAttribute('data-initial-order')) || 0) - (parseInt(b.getAttribute('data-initial-order')) || 0);
1470            }
1471            if (sortKey === 'popular') {
1472                const likesA = parseInt(a.getAttribute('data-likes')) || 0;
1473                const likesB = parseInt(b.getAttribute('data-likes')) || 0;
1474                if (likesB !== likesA) return likesB - likesA;
1475                return (parseInt(a.getAttribute('data-initial-order')) || 0) - (parseInt(b.getAttribute('data-initial-order')) || 0);
1476            }
1477            if (sortKey === 'name_asc') {
1478                const nameA = a.getAttribute('data-name') || '';
1479                const nameB = b.getAttribute('data-name') || '';
1480                const cmp = nameA.localeCompare(nameB, LOCALE_CODE, { numeric: true, sensitivity: 'base' });
1481                if (cmp !== 0) return cmp;
1482                return (parseInt(a.getAttribute('data-initial-order')) || 0) - (parseInt(b.getAttribute('data-initial-order')) || 0);
1483            }
1484            if (sortKey === 'name_desc') {
1485                const nameA = a.getAttribute('data-name') || '';
1486                const nameB = b.getAttribute('data-name') || '';
1487                const cmp = nameB.localeCompare(nameA, LOCALE_CODE, { numeric: true, sensitivity: 'base' });
1488                if (cmp !== 0) return cmp;
1489                return (parseInt(a.getAttribute('data-initial-order')) || 0) - (parseInt(b.getAttribute('data-initial-order')) || 0);
1490            }
1491            // Padrão / Relevância da Busca: preserva a ordem natural de chegada/offset
1492            return (parseInt(a.getAttribute('data-initial-order')) || 0) - (parseInt(b.getAttribute('data-initial-order')) || 0);
1493        });
1494
1495        cards.forEach(c => container.appendChild(c));
1496    }
1497
1498    function toggleSize(sz) {
1499        sz = normalizeSize(sz);
1500        if (state.activeSizes.has(sz)) state.activeSizes.delete(sz);
1501        else state.activeSizes.add(sz);
1502        apply();
1503    }
1504
1505    function toggleColor(clr) {
1506        clr = canonicalColorSlug(clr);
1507        if (state.activeColors.has(clr)) state.activeColors.delete(clr);
1508        else state.activeColors.add(clr);
1509        apply();
1510    }
1511
1512    function toggleType(t) {
1513        if (state.activeTypes.has(t)) state.activeTypes.delete(t);
1514        else state.activeTypes.add(t);
1515        apply();
1516    }
1517
1518    function toggleMeta(k, v) {
1519        const token = `${k}:::${v}`;
1520        if (state.activeMeta.has(token)) state.activeMeta.delete(token);
1521        else state.activeMeta.add(token);
1522        apply();
1523    }
1524
1525    function toggleBrand(b) {
1526        if (state.activeBrands.has(b)) state.activeBrands.delete(b);
1527        else state.activeBrands.add(b);
1528        apply();
1529    }
1530
1531    function toggleCollection(col) {
1532        if (state.activeCollections.has(col)) state.activeCollections.delete(col);
1533        else state.activeCollections.add(col);
1534        apply();
1535    }
1536
1537    function toggleDeal(dealKey) {
1538        if (state.activeDeals.has(dealKey)) state.activeDeals.delete(
1538dealKey);
1539        else state.activeDeals.add(dealKey);
1540        apply();
1541    }
1542
1543    function setSort(val) {
1544        state.sort = val || 'default';
1545        const sortSelect = document.getElementById('pop-catalog-sort-select') || document.getElementById('catalog-sort-select') || document.getElementById('sort-by-select');
1546        if (sortSelect && sortSelect.value !== state.sort) {
1547            sortSelect.value = state.sort;
1548        }
1549        apply();
1550    }
1551
1552    function clearAll() {
1553        state.activeSizes.clear();
1554        state.activeColors.clear();
1555        state.activeTypes.clear();
1556        state.activeMeta.clear();
1557        state.activeBrands.clear();
1558        state.activeCollections.clear();
1559        state.activeDeals.clear();
1560        state.priceMin = null;
1561        state.priceMax = null;
1562        syncAllPriceSliders(state.boundsMin, state.boundsMax);
1563        apply();
1564    }
1565
1566    function openDrawer() {
1567        const backdrop = document.getElementById('mobile-filter-drawer-backdrop') || document.getElementById('pop-catalog-mobile-drawer-backdrop');
1568        const drawer = document.getElementById('pop-catalog-mobile-drawer') || document.getElementById('mobile-filter-drawer');
1569        if (backdrop) backdrop.classList.add('is-open');
1570        if (drawer) drawer.classList.add('is-open');
1571        document.body.style.overflow = 'hidden';
1572
1573        const mContainer = document.getElementById('mobile-filters-container');
1574        const dContainer = document.getElementById('desktop-filters-container');
1575        if (mContainer && (!mContainer.children.length || !mContainer.querySelector('.filter-group-price'))) {
1576            if (dContainer && dContainer.innerHTML.trim().length > 0) {
1577                mContainer.innerHTML = dContainer.innerHTML;
1578            }
1579        }
1580
1581        syncActivePillClasses();
1582        syncAllPriceSliders(state.priceMin !== null ? state.priceMin : state.boundsMin, state.priceMax !== null ? state.priceMax : state.boundsMax);
1583    }
1584
1585    function closeDrawer() {
1586        const backdrop = document.getElementById('mobile-filter-drawer-backdrop') || document.getElementById('pop-catalog-mobile-drawer-backdrop');
1587        const drawer = document.getElementById('pop-catalog-mobile-drawer') || document.getElementById('mobile-filter-drawer');
1588        if (backdrop) backdrop.classList.remove('is-open');
1589        if (drawer) drawer.classList.remove('is-open');
1590        document.body.style.overflow = '';
1591    }
1592
1593    let infiniteObserver = null;
1594
1595    /**
1596     * Carregamento sob demanda / Scroll Infinito progressivo e suave
1597     * Busca os próximos produtos apenas quando o usuário se aproxima do final da página
1598     */
1599    async function fetchNextBatch() {
1600        const loader = document.getElementById('pop-catalog-stream-loader') || document.getElementById('catalog-stream-loader');
1601        const container = document.getElementById('pop-catalog-grid') || document.getElementById('products-grid-container') || document.querySelector('.products-grid-container');
1602        if (!loader || !container || state.isStreaming) return;
1603
1604        let currentOffset = parseInt(loader.getAttribute('data-offset')) || 0;
1605        const chunkSize = parseInt(loader.getAttribute('data-chunk-size')) || 36;
1606        const totalCount = parseInt(loader.getAttribute('data-total-count')) || 0;
1607        const baseUrl = loader.getAttribute('data-base-url');
1608
1609        if (!baseUrl || currentOffset >= totalCount) {
1610            loader.style.display = 'none';
1611            return;
1612        }
1613
1614        state.isStreaming = true;
1615
1616        try {
1617            const streamUrl = `${baseUrl}&offset=${currentOffset}&limit=${chunkSize}`;
1618            const res = await fetch(streamUrl);
1619            if (!res.ok) throw new Error('HTTP ' + res.status);
1620
1621            const htmlCards = await res.text();
1622            if (!htmlCards || htmlCards.trim().length === 0) {
1623                loader.style.display = 'none';
1624                return;
1625            }
1626
1627            const tempDiv = document.createElement('div');
1628            tempDiv.innerHTML = htmlCards;
1629            const newCards = Array.from(tempDiv.children);
1630
1631            if (newCards.length === 0) {
1632                loader.style.display = 'none';
1633                return;
1634            }
1635
1636            // Anexa os novos cards no final do container
1637            const fragment = document.createDocumentFragment();
1638            newCards.forEach(c => {
1639                c.style.display = '';
1640                fragment.appendChild(c);
1641            });
1642            container.appendChild(fragment);
1643
1644            currentOffset += newCards.length;
1645            loader.setAttribute('data-offset', currentOffset);
1646            loader.setAttribute('data-loaded-count', currentOffset);
1647
1648            const statusText = document.getElementById('stream-status-text');
1649            if (statusText) {
1650                statusText.innerHTML = `${tJs('loadingMore')} (<b>${Math.min(currentOffset, totalCount)}</b> ${tJs('of')} <b>${totalCount}</b>)`;
1651            }
1652
1653            if (currentOffset >= totalCount) {
1654                loader.style.transition = 'opacity 0.25s ease';
1655                loader.style.opacity = '0';
1656                setTimeout(() => {
1657                    loader.style.display = 'none';
1658                    loader.style.opacity = '1';
1659                }, 250);
1660            }
1661
1662            if (window.PopStoreWishlist && typeof PopStoreWishlist.syncCards === 'function') {
1663                PopStoreWishlist.syncCards(container);
1664            }
1665
1666            if (typeof initCardVideos === 'function') {
1667                initCardVideos(container);
1668            }
1669
1670            if (typeof lucide !== 'undefined') {
1671                lucide.createIcons();
1672            }
1673
1674        } catch (err) {
1675            console.error('Erro no scroll infinito do catálogo:', err);
1676        } finally {
1677            state.isStreaming = false;
1678        }
1679    }
1680
1681    /**
1682     * Configura o IntersectionObserver para scroll infinito
1683     */
1684    function setupInfiniteScroll() {
1685        const loader = document.getElementById('pop-catalog-stream-loader') || document.getElementById('catalog-stream-loader');
1686        if (!loader) return;
1687
1688        if (infiniteObserver) {
1689            infiniteObserver.disconnect();
1690        }
1691
1692        loader.style.cursor = 'pointer';
1693        loader.onclick = () => fetchNextBatch();
1694
1695        if ('IntersectionObserver' in window) {
1696            infiniteObserver = new IntersectionObserver((entries) => {
1697                entries.forEach(entry => {
1698                    if (entry.isIntersecting && !state.isStreaming) {
1699                        fetchNextBatch();
1700                    }
1701                });
1702            }, {
1703                root: null,
1704                rootMargin: '450px 0px 450px 0px',
1705                threshold: 0.01
1706            });
1707
1708            infiniteObserver.observe(loader);
1709        }
1710    }
1711
1712    return {
1713        init: function() {
1714            // Inicializa limites reais a partir do container SSR se presente
1715            const priceContainer = document.querySelector('.filter-price-slider-container');
1716            if (priceContainer) {
1717                const bMinAttr = parseFloat(priceContainer.getAttribute('data-min'));
1718                const bMaxAttr = parseFloat(priceContainer.getAttribute('data-max'));
1719                if (!isNaN(bMinAttr)) state.boundsMin = bMinAttr;
1720                if (!isNaN(bMaxAttr)) state.boundsMax = bMaxAttr;
1721            }
1722
1723            loadStateFromUrl();
1724
1725            // Fallback de segurança: se por qualquer razão o servidor não gerou blocos SSR, popula via JS
1726            const dContainer = document.getElementById('desktop-filters-container');
1727            if (dContainer && (!dContainer.children.length || !dContainer.querySelector('.filter-group-block'))) {
1728                dContainer.innerHTML = renderFiltersUI();
1729            }
1730
1731            // Sincroniza visualmente os sliders de preço com os valores da URL e limites da página
1732            const initValMin = state.priceMin !== null ? state.priceMin : state.boundsMin;
1733            const initValMax = state.priceMax !== null ? state.priceMax : state.boundsMax;
1734            syncAllPriceSliders(initValMin, initValMax);
1735
1736            syncActivePillClasses();
1737            renderActiveTags();
1738
1739            window.addEventListener('popstate', () => {
1740                loadStateFromUrl();
1741                apply(false);
1742            });
1743
1744            setupInfiniteScroll();
1745        },
1746        toggleSize,
1747        toggleColor,
1748        toggleType,
1749        toggleMeta,
1750        toggleBrand,
1751        toggleCollection,
1752        toggleDeal,
1753        handlePriceInput,
1754        applyPriceFilter,
1755        setSort,
1756        clearAll,
1757        openDrawer,
1758        closeDrawer,
1759        removeTag
1760    };
1761})();
1762
1763// Inicializa o motor de filtros assim que o DOM estiver pronto
1764if (document.readyState === 'loading') {
1765    document.addEventListener('DOMContentLoaded', () => PopStoreFilterEngine.init());
1766} else {
1767    PopStoreFilterEngine.init();
1768}
1769
1770if (typeof lucide !== 'undefined') {
1771    lucide.createIcons();
1772}

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.