PageSourceSearch

https://nativafmbauru.com.br/assets/js/app.js?v=1790900782

js nativafmbauru.com.br collected 2026-10-02 00:26:38 UTC 27,408 bytes, 718 lines download raw bytes

1/**
2 * Main Application JavaScript
3 * 
4 * Handles navigation, mobile menu, and general UI interactions.
5 */
6
7document.addEventListener('DOMContentLoaded', function () {
8    // ============================================
9    // SPA Navigation (keeps player playing)
10    // ============================================
11    initSPANavigation();
12
13    // ============================================
14    // Mobile Menu Toggle
15    // ============================================
16    const mobileMenuBtn = document.getElementById('mobileMenuBtn');
17    const mainNav = document.getElementById('mainNav');
18    const menuOverlay = document.getElementById('menuOverlay');
19
20    if (mobileMenuBtn && mainNav) {
21        mobileMenuBtn.addEventListener('click', function () {
22            mainNav.classList.toggle('active');
23            if (menuOverlay) menuOverlay.classList.toggle('active');
24
25            // Update icon
26            const icon = this.querySelector('i');
27            if (mainNav.classList.contains('active')) {
28                icon.className = 'fas fa-times';
29            } else {
30                icon.className = 'fas fa-bars';
31            }
32        });
33
34        // Close menu when clicking outside or on overlay
35        document.addEventListener('click', function (e) {
36            if (!mainNav.contains(e.target) && !mobileMenuBtn.contains(e.target) && mainNav.classList.contains('active')) {
37                mainNav.classList.remove('active');
38                if (menuOverlay) menuOverlay.classList.remove('active');
39                mobileMenuBtn.querySelector('i').className = 'fas fa-bars';
40            }
41        });
42
43        // Explicit overlay click
44        if (menuOverlay) {
45            menuOverlay.addEventListener('click', function () {
46                mainNav.classList.remove('active');
47                this.classList.remove('active');
48                mobileMenuBtn.querySelector('i').className = 'fas fa-bars';
49            });
50        }
51
52        // Close menu on link click
53        mainNav.querySelectorAll('a').forEach(link => {
54            link.addEventListener('click', () => {
55                mainNav.classList.remove('active');
56                if (menuOverlay) menuOverlay.classList.remove('active');
57                mobileMenuBtn.querySelector('i').className = 'fas fa-bars';
58            });
59        });
60    }
61
62    // ============================================
63    // Header Scroll Effect
64    // ============================================
65    const header = document.getElementById('header');
66    let lastScroll = 0;
67
68    window.addEventListener('scroll', function () {
69        const currentScroll = window.pageYOffset;
70
71        if (currentScroll > 50) {
72            header.classList.add('header-scrolled');
73        } else {
74            header.classList.remove('header-scrolled');
75        }
76
77        lastScroll = currentScroll;
78    });
79
80    // ============================================
81    // Espaçamento dinâmico abaixo do header
82    // ============================================
83    adjustHeaderSpacing();
84    window.addEventListener('resize', adjustHeaderSpacing);
85    // Aguarda o logo carregar para recalcular a altura real
86    window.addEventListener('load', adjustHeaderSpacing);
87
88    // ============================================
89    // Hero Slider
90    // ============================================
91    initHeroSlider();
92
93    // ============================================
94    // Forms
95    // ============================================
96    initForms();
97
98    // ============================================
99    // Lazy Loading Images
100    // ============================================
101    initLazyLoading();
102});
103
104/**
105 * Compensa a altura real do header fixo no conteúdo abaixo dele.
106 * Usa paddingTop no #mainContent (sobrescreve o valor CSS estático via inline style)
107 * e zera o spacer físico quando presente (demo01/demo05), evitando duplo espaçamento.
108 */
109function adjustHeaderSpacing() {
110    const header = document.getElementById('header');
111    const mainContent = document.getElementById('mainContent');
112    if (!header || !mainContent) return;
113
114    const h = header.offsetHeight;
115
116    // Zera spacer físico (demo01/demo05) para não acumular com o paddingTop abaixo
117    const spacer = document.getElementById('header-spacer');
118    if (spacer) spacer.style.height = '0';
119
120    // Respiro superior: nas páginas internas (não na home) separa o conteúdo do
121    // header fixo, evitando que capa/título fiquem colados logo abaixo dele.
122    // A home fica sem respiro — usa sliders full-bleed que passam sob o header.
123    const isHome = document.body.classList.contains('page-home');
124    const breathing = isHome ? 0 : (window.innerWidth <= 768 ? 16 : 24);
125
126    mainContent.style.paddingTop = (h + breathing) + 'px';
127}
128
129/**
130 * Execute scripts in dynamically loaded content
131 * Scripts inserted via innerHTML don't execute, so we need to recreate them
132 */
133function executeScripts(container) {
134    const scripts = container.querySelectorAll('script');
135    scripts.forEach(oldScript => {
136        const newScript = document.createElement('script');
137
138        // Copy all attributes
139        Array.from(oldScript.attributes).forEach(attr => {
140            newScript.setAttribute(attr.name, attr.value);
141        });
142
143        // Copy inline content
144        newScript.textContent = oldScript.textContent;
145
146        // Replace old script with new one (this causes it to execute)
147        oldScript.parentNode.replaceChild(newScript, oldScript);
148    });
149}
150
151/**
152 * Initialize SPA Navigation
153 * Intercepts link clicks and loads content via AJAX to keep player playing
154 */
155function initSPANavigation() {
156    const mainContent = document.getElementById('mainContent');
157
158    // Intercept all internal link clicks
159    document.addEventListener('click', function (e) {
160        const link = e.target.closest('a');
161
162        if (!link) return;
163
164        let href = link.getAttribute('href');
165
166        if (!href) return;
167
168        // Skip anchor links, admin links, download links, and links with target
169        if (href.startsWith('#') ||
170            href.startsWith('javascript:') ||
171            href.includes('/sistema') ||
172            link.hasAttribute('download') ||
173            link.hasAttribute('target')) {
174            return;
175        }
176
177        // Skip WhatsApp widget elements and Life Search Links
178        if (link.closest('.whatsapp-fab') || link.closest('.whatsapp-panel') || link.closest('.live-search-item') || link.closest('.live-search-footer')) {
179            return;
180        }
181
182        // Skip if it's a file link
183        if (href.match(/\.(pdf|zip|doc|docx|xls|xlsx)$/i)) {
184            return;
185        }
186
187        // Check if it's an internal URL (same site)
188        let isInternal = false;
189
190        // Handle absolute URLs from the same site
191        if (href.startsWith(SITE_URL)) {
192            isInternal = true;
193            // Convert to relative path for consistent handling
194            href = href.replace(SITE_URL, '') || '/';
195        }
196        // Handle relative URLs
197        else if (!href.startsWith('http') && !href.startsWith('//')) {
198            isInternal = true;
199        }
200        // Skip truly external links
201        else {
202            return;
203        }
204
205        if (!isInternal) return;
206
207        e.preventDefault();
208
209        // Reconstruct full URL for browser history
210        const fullUrl = href.startsWith('/') || href.startsWith('./')
211            ? SITE_URL + href.replace('./', '/').replace(/^\//, '/')
212            : SITE_URL + '/' + href;
213
214        // Load the page via AJAX
215        loadPage(fullUrl);
216    });
217
218    // Handle browser back/forward buttons
219    window.addEventListener('popstate', function (e) {
220        if (e.state && e.state.href) {
221            loadPage(e.state.href, false);
222        } else {
223            loadPage(window.location.href, false);
224        }
225    });
226}
227
228/**
229 * Navega para uma URL do próprio site SEM recarregar o documento.
230 *
231 * Qualquer navegação dura — location.href, window.open(url, '_self'), ou um
232 * link que a SPA se recuse a interceptar — destrói o documento e, junto com
233 * ele, a instância do RadioPlayer: a transmissão ao vivo para. Cliques em <a>
234 * internos já passam por initSPANavigation(); este helper é o mesmo caminho
235 * para cliques que NÃO nascem de um link (ex.: a área clicável do slide do
236 * hero slider), aplicando exatamente as mesmas exclusões.
237 *
238 * @param {string} url      URL absoluta ou relativa ao site
239 * @param {string} [target] '_blank' abre em nova aba (documento atual intacto)
240 * @returns {boolean} true se a navegação foi resolvida sem recarregar a página
241 */
242function spaNavigate(url, target) {
243    if (!url) return false;
244
245    // Nova aba: o documento atual não é tocado, o player segue tocando.
246    if (target === '_blank') {
247        window.open(url, '_blank', 'noopener,noreferrer');
248        return true;
249    }
250
251    const isExternal = /^(https?:)?\/\//i.test(url) && url.indexOf(SITE_URL) !== 0;
252    const isSpecial  = url.charAt(0) === '#' || (/^[a-z][a-z0-9+.-]*:/i.test(url) && !/^https?:/i.test(url));
253    const isAdmin    = url.indexOf('/sistema') !== -1;
254    const isFile     = /\.(pdf|zip|doc|docx|xls|xlsx)$/i.test(url.split('?')[0]);
255
256    if (isExternal || isSpecial || isAdmin || isFile || typeof loadPage !== 'function') {
257        window.location.href = url;
258        return false;
259    }
260
261    const relative = url.indexOf(SITE_URL) === 0 ? url.slice(SITE_URL.length) : url;
262    loadPage(SITE_URL.replace(/\/+$/, '') + '/' + relative.replace(/^\/+/, ''));
263    return true;
264}
265window.spaNavigate = spaNavigate;
266
267/**
268 * Load page content via AJAX
269 */
270async function loadPage(href, pushState = true) {
271    const mainContent = document.getElementById('mainContent');
272
273    // Parse the URL to get page parameter
274    let apiUrl;
275
276    // Remove base URL if present to get relative path
277    let path = href;
278    if (path.startsWith(SITE_URL)) {
279        path = path.replace(SITE_URL, '');
280    }
281
282    // Separate path from query string
283    let queryString = '';
284    if (path.includes('?')) {
285        const parts = path.split('?');
286        path = parts[0];
287        queryString = parts[1] || '';
288    }
289
290    // Remove leading and trailing slashes from path
291    path = path.replace(/^\/+/, '').replace(/\/+$/, '');
292
293    // Home page detection - but check for query params first!
294    // isHomeNav controla o respiro superior: home não recebe respiro (slider full-bleed).
295    let isHomeNav = false;
296    if (queryString && queryString.includes('page=')) {
297        // Legacy URL format with query string containing page param
298        const urlParams = new URLSearchParams(queryString);
299        const page = urlParams.get('page') || 'home';
300        const slug = urlParams.get('slug') || '';
301        const category = urlParams.get('category') || '';
302        apiUrl = SITE_URL + '/api/page.php?page=' + page;
303        if (slug) apiUrl += '&slug=' + slug;
304        if (category) apiUrl += '&category=' + category;
305        isHomeNav = (page === 'home');
306    } else if (path === '' || path === './' || path === '/' || path === 'index.php' || path === 'home') {
307        apiUrl = SITE_URL + '/api/page.php?page=home';
308        // Normalizar href para a raiz, evitando que "/home" apareça na barra do browser
309        href = SITE_URL + '/';
310        isHomeNav = true;
311    } else if (path.includes('/')) {
312        // Friendly URL format with slug (noticia/slug-here or promocao/slug-here)
313        const parts = path.split('/');
314        const page = parts[0];
315        const slug = parts[1];
316        apiUrl = SITE_URL + '/api/page.php?page=' + page + '&slug=' + slug;
317    } else {
318        // Simple page name (noticias, equipe, eventos, etc.)
319        apiUrl = SITE_URL + '/api/page.php?page=' + path;
320        // Preserve any query params (like category filter)
321        if (queryString) {
322            apiUrl += '&' + queryString;
323        }
324    }
325
326    // CRITICAL FIX: Limpar completamente o estado anterior antes de carregar nova página
327    // 1. Parar todos os players ativos
328    if (window.previewPlayer) {
329        window.previewPlayer.pause();
330        window.previewPlayer.currentTime = 0;
331    }
332
333    // Stop all chart audio players
334    document.querySelectorAll('audio.chart-audio-player').forEach(audio => {
335        audio.pause();
336        audio.currentTime = 0;
337    });
338
339    // 2. Limpar timers/intervals de sliders
340    if (window.heroSliderInterval) {
341        clearInterval(window.heroSliderInterval);
342        window.heroSliderInterval = null;
343    }
344
345    // Show loading state - Keep content but fade it
346    mainContent.style.opacity = '0.4';
347    mainContent.style.transition = 'opacity 0.3s ease';
348
349    try {
350        const response = await fetch(apiUrl);
351        const html = await response.text();
352
353        // === Preservar o player de vídeo global entre navegações SPA ===
354        // O vídeo (.video-mobile-section + #vmpWidget) e seu CSS (#vpgStyles) vivem
355        // dentro de #mainContent, mas NÃO devem ser destruídos/reiniciados ao trocar
356        // de página. Detacha os nós vivos (mantém o HLS tocando) antes de limpar e
357        // reinsere depois — tudo síncrono (sem await no meio) para o vídeo não pausar.
358        // O CSS PRECISA ser preservado também, senão o #vmpWidget perde
359        // position:fixed/width e renderiza gigante inline.
360        const _preservedVideoSection = mainContent.querySelector('.video-mobile-section');
361        const _preservedVideoWidget  = document.getElementById('vmpWidget');
362        const _preservedVideoStyles  = document.getElementById('vpgStyles');
363        if (_preservedVideoSection) _preservedVideoSection.remove();
364        if (_preservedVideoWidget)  _preservedVideoWidget.remove();
365        if (_preservedVideoStyles)  _preservedVideoStyles.remove();
366
367        // ONLY clear and update content AFTER data is received
368        // 3. Limpar completamente o conteúdo e classes
369        mainContent.innerHTML = '';
370        mainContent.className = '';
371        mainContent.removeAttribute('style');
372
373        // Update content
374        mainContent.innerHTML = html;
375
376        // Remover qualquer player de vídeo vindo no HTML da nova página (evita
377        // duplicar com o preservado) e reinserir o player vivo + CSS no topo do fluxo.
378        mainContent.querySelectorAll('.video-mobile-section, #vmpWidget, #vpgStyles').forEach(function (el) { el.remove(); });
379        if (_preservedVideoStyles)  mainContent.appendChild(_preservedVideoStyles);
380        if (_preservedVideoWidget)  mainContent.appendChild(_preservedVideoWidget);
381        if (_preservedVideoSection) mainContent.insertBefore(_preservedVideoSection, mainContent.firstChild);
382
383        // Restore opacity
384        mainContent.style.opacity = '1';
385
386        // Execute any scripts in the loaded content
387        executeScripts(mainContent);
388
389        // Update URL
390        if (pushState) {
391            history.pushState({ href: href }, '', href);
392        }
393
394        // Update active nav link
395        updateActiveNavLink(href);
396
397        // Scroll to top
398        window.scrollTo({ top: 0, behavior: 'smooth' });
399
400        // Atualiza a classe de página (home vs interna) e recalcula o respiro superior.
401        // O removeAttribute('style') acima apagou o paddingTop inline — reaplicamos aqui
402        // para manter a altura real do header + o respiro das páginas internas.
403        document.body.classList.toggle('page-home', isHomeNav);
404        adjustHeaderSpacing();
405
406        // Reinitialize page components
407        initHeroSlider();
408        initForms();
409        initLazyLoading();
410
411        // ── Analytics: registrar pageview da navegação SPA ──
412        if (typeof window.__analyticsTrack === 'function') {
413            // Extrair o pathname real da URL navegada
414            var trackedUrl = href;
415            if (trackedUrl.startsWith(SITE_URL)) {
416                trackedUrl = trackedUrl.replace(SITE_URL, '') || '/';
417            }
418            window.__analyticsTrack(trackedUrl, document.title);
419        }
420
421    } catch (error) {
422        console.error('Navigation error:', error);
423        mainContent.style.opacity = '1';
424        showNotification('Erro ao carregar página', 'error');
425    }
426}
427
428/**
429 * Update active state on navigation links
430 */
431function updateActiveNavLink(href) {
432    const nav = document.getElementById('mainNav');
433    if (!nav) return;
434
435    // Remove all active classes
436    nav.querySelectorAll('.nav-link').forEach(link => {
437        link.classList.remove('active');
438    });
439
440    // Find and activate matching link
441    nav.querySelectorAll('.nav-link').forEach(link => {
442        const linkHref = link.getAttribute('href');
443        if (linkHref === href ||
444            (href === './' && linkHref === './') ||
445            (href.includes('page=' + linkHref.split('page=')[1]))) {
446            link.classList.add('active');
447        }
448    });
449
450    // Sincroniza estados dos temas (Neon, Wave, Pulse) após navegação SPA
451    // pushState já atualizou window.location.pathname antes desta chamada
452    ['syncNeonNavActive', 'syncWaveNavActive', 'syncPulseNavActive'].forEach(fn => {
453        if (typeof window[fn] === 'function') window[fn]();
454    });
455}
456
457/**
458 * Initialize Hero Slider
459 * Note: home.php has its own enhanced slider that handles title/text
460 * This function clears any existing interval and sets up a basic slider
461 * for pages that might have slides but no custom init
462 */
463function initHeroSlider() {
464    // Clear any existing interval to prevent duplicates when navigating
465    if (window.heroSliderInterval) {
466        clearInterval(window.heroSliderInterval);
467        window.heroSliderInterval = null;
468    }
469
470    const slides = document.querySelectorAll('.hero-slide');
471    if (slides.length <= 1) return;
472
473    // Check if home.php's slider script has already run
474    // (home.php sets window.heroSliderInterval when it starts)
475    // Only initialize if there's no existing interval
476    if (window.heroSliderInterval) return;
477
478    let currentSlide = 0;
479
480    function showSlide(index) {
481        slides.forEach((slide, i) => {
482            slide.classList.toggle('active', i === index);
483        });
484    }
485
486    function nextSlide() {
487        currentSlide = (currentSlide + 1) % slides.length;
488        showSlide(currentSlide);
489    }
490
491    // Auto-advance every 5 seconds
492    window.heroSliderInterval = setInterval(nextSlide, 5000);
493}
494
495/**
496 * Initialize Forms
497 * Nota: o formulário de promoção (#promotionForm) tem seu próprio listener
498 * registrado diretamente em promotion-single.php — não registrar aqui para
499 * evitar handlers duplicados que causam falsos "Erro de conexão".
500 */
501function initForms() {
502    // Formulário de promoção gerenciado pelo próprio template (promotion-single.php)
503}
504
505/**
506 * Initialize Lazy Loading
507 */
508function initLazyLoading() {
509    const images = document.querySelectorAll('img[data-src]');
510
511    if ('IntersectionObserver' in window) {
512        const observer = new IntersectionObserver((entries) => {
513            entries.forEach(entry => {
514                if (entry.isIntersecting) {
515                    const img = entry.target;
516                    img.src = img.dataset.src;
517                    img.removeAttribute('data-src');
518                    observer.unobserve(img);
519                }
520            });
521        });
522
523        images.forEach(img => observer.observe(img));
524    } else {
525        // Fallback for older browsers
526        images.forEach(img => {
527            img.src = img.dataset.src;
528        });
529    }
530}
531
532/**
533 * Show Notification
534 */
535function showNotification(message, type = 'info') {
536    // Create notification element
537    const notification = document.createElement('div');
538    notification.className = `notification notification-${type}`;
539    notification.innerHTML = `
540        <i class="fas fa-${type === 'success' ? 'check-circle' : type === 'error' ? 'exclamation-circle' : 'info-circle'}"></i>
541        <span>${message}</span>
542    `;
543
544    // Add styles if not exists
545    if (!document.getElementById('notification-styles')) {
546        const styles = document.createElement('style');
547        styles.id = 'notification-styles';
548        styles.textContent = `
549            .notification {
550                position: fixed;
551                top: 90px;
552                right: 20px;
553                padding: 15px 25px;
554                background: var(--bg-card);
555                border: 1px solid var(--border-color);
556                border-radius: var(--radius-md);
557                display: flex;
558                align-items: center;
559                gap: 12px;
560                z-index: 10000;
561                animation: slideInRight 0.3s ease;
562                box-shadow: var(--shadow-lg);
563            }
564            .notification-success { border-left: 4px solid var(--success); }
565            .notification-success i { color: var(--success); }
566            .notification-error { border-left: 4px solid var(--danger); }
567            .notification-error i { color: var(--danger); }
568            @keyframes slideInRight {
569                from { transform: translateX(100%); opacity: 0; }
570                to { transform: translateX(0); opacity: 1; }
571            }
572        `;
573        document.head.appendChild(styles);
574    }
575
576    document.body.appendChild(notification);
577
578    // Remove after 4 seconds
579    setTimeout(() => {
580        notification.style.animation = 'slideInRight 0.3s ease reverse';
581        setTimeout(() => notification.remove(), 300);
582    }, 4000);
583}
584
585/**
586 * Format date to Brazilian format
587 */
588function formatDate(dateStr) {
589    const date = new Date(dateStr);
590    return date.toLocaleDateString('pt-BR');
591}
592
593/**
594 * Format datetime to Brazilian format
595 */
596function formatDateTime(dateStr) {
597    const date = new Date(dateStr);
598    return date.toLocaleString('pt-BR');
599}
600
601/**
602 * Live Search AJAX
603 */
604document.addEventListener("DOMContentLoaded", () => {
605    function setupLiveSearch(inputId, formId, dropdownId, spinnerId) {
606        const input = document.getElementById(inputId);
607        const form = document.getElementById(formId);
608        const dropdown = document.getElementById(dropdownId);
609        let spinner = document.getElementById(spinnerId);
610        if (!spinner && form) {
611            spinner = form.querySelector('.neon-search-spinner');
612        }
613        
614        if (!input || !dropdown) return;
615        
616        let debounceTimer;
617
618        input.addEventListener("input", (e) => {
619            const query = e.target.value.trim();
620            
621            clearTimeout(debounceTimer);
622            
623            if (query.length < 2) {
624                dropdown.innerHTML = "";
625                dropdown.classList.remove("is-active");
626                if (spinner) spinner.style.display = "none";
627                return;
628            }
629
630            if (spinner) spinner.style.display = "block";
631
632            debounceTimer = setTimeout(() => {
633                fetch(`${SITE_URL}/api/search.php?q=${encodeURIComponent(query)}`)
634                    .then(res => res.json())
635                    .then(data => {
636                        if (spinner) spinner.style.display = "none";
637                        
638                        dropdown.innerHTML = "";
639                        
640                        if (data.results && data.results.length > 0) {
641                            data.results.forEach(item => {
642                                const a = document.createElement("a");
643                                a.href = item.url;
644                                a.className = "live-search-item";
645                                const thumbHtml = item.image
646                                    ? `<img src="${item.image}" alt="${item.title}" class="search-result-thumb" onerror="this.parentElement.classList.add('no-thumb'); this.remove();">`
647                                    : `<div class="search-result-thumb search-result-thumb--placeholder"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg></div>`;
648                                a.innerHTML = `
649                                    ${thumbHtml}
650                                    <div class="live-search-text">
651                                        <span class="live-search-title">${item.title}</span>
652                                        <span class="live-search-type">${item.type}</span>
653                                    </div>
654                                `;
655                                dropdown.appendChild(a);
656                            });
657                            
658                            const allResults = document.createElement("a");
659                            allResults.href = document.querySelector("#" + formId)?.action + "?page=busca&q=" + encodeURIComponent(query);
660                            allResults.className = "live-search-footer";
661                            allResults.innerText = `Ver todos os ${data.count} resultados da busca ...`;
662                            dropdown.appendChild(allResults);
663                            
664                        } else {
665                            dropdown.innerHTML = `<div class="live-search-empty">Nenhuma rádio, notícia ou resultado encontrado.</div>`;
666                        }
667                        
668                        dropdown.classList.add("is-active");
669                    })
670                    .catch(e => {
671                        if (spinner) spinner.style.display = "none";
672                        console.error("Erro na busca:", e);
673                    });
674            }, 350);
675        });
676
677        document.addEventListener("click", (e) => {
678            if (form && !form.contains(e.target) && !dropdown.contains(e.target)) {
679                dropdown.classList.remove("is-active");
680            }
681        });
682        
683        input.addEventListener("focus", () => {
684            if (input.value.trim().length >= 2 && dropdown.innerHTML !== "") {
685                dropdown.classList.add("is-active");
686            }
687        });
688    }
689
690    setupLiveSearch("neonDesktopSearchInput", "neonDesktopSearchForm", "neonDesktopSearchDropdown", null);
691    setupLiveSearch("neonMobileSearchInput", "neonMobileSearchForm", "neonMobileSearchDropdown", "neonMobileSearchSpinner");
692});
693
694/**
695 * Update Theme Color Meta Tag
696 * Update top nav browser color when switching standard mode/themes
697 */
698window.updateThemeColor = function() {
699    const themeColorMeta = document.querySelector('meta[name="theme-color"]');
700    if (!themeColorMeta) return;
701
702    // Ler a cor de fundo do header prioritariamente (agora sendo sólida, idêntica) 
703    // ou cair como fallback no bg-primary do root document.
704    let headerBg = getComputedStyle(document.documentElement).getPropertyValue('--bg-primary').trim();
705    
706    // Se conseguirmos uma cor computada e sólida (rgb) do próprio header, preferiremos usá-la.
707    const headerNode = document.querySelector('header.neon-header, header');
708    if (headerNode) {
709        const computedHeader = getComputedStyle(headerNode).backgroundColor;
710        if (computedHeader && computedHeader !== 'transparent' && computedHeader !== 'rgba(0, 0, 0, 0)') {
711            headerBg = computedHeader;
712        }
713    }
714
715    if (headerBg) {
716        themeColorMeta.setAttribute('content', headerBg);
717    }
718};

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.