PageSourceSearch

https://cbnamazonia.com/noticias

html cbnamazonia.com collected 2026-10-03 20:25:16 UTC 91,172 bytes, 1,603 lines download raw bytes

1<!DOCTYPE html>
2<html lang="pt-BR">
3
4<head>
4<script>window.__TENANT__={"kind":"portal","match":true,"source":"portal_domains","status":"active","hostname":"cbnamazonia.com","launched":true,"org_name":"Rede Amazônica","org_slug":"rede-amazonica","portal_id":"cbn-amazonia","dns_target":null,"is_primary":true,"product_line":"platform","access_blocked":false,"organization_id":"b1eebc99-9c0b-4ef8-bb6d-6bb9bd380a22","legacy_full_platform":true};</script>
4
5  <meta charset="UTF-8" />
6  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
7  <!-- iPad/iOS PWA — Add to Home Screen -->
8  <meta name="mobile-web-app-capable" content="yes" />
9  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
10  <meta name="apple-mobile-web-app-title" content="" />
11  <link rel="apple-touch-icon" sizes="180x180" id="apple-touch-icon" href="" />
12  <title>Últimas Notícias - CBN Amazônia</title>
13  <meta name="description" content="As últimas notícias de CBN Amazônia. Cobertura jornalística completa e atualizada sobre Amazônia Legal com credibilidade e agilidade." />
14  <!-- Bing Webmaster verification is injected per-portal by
15       netlify/edge-functions/seo-meta-injection.ts (reading
16       portal_seo_config.verification.bing from the DB, landing in PR2).
17       The single hardcoded JB key that used to live here served all
18       tenants the same value — leaking JB ownership into Bing for every
19       portal under the same Netlify deploy. -->
20  <link id="favicon" rel="icon" type="image/png" href="" />
21  <!-- Preconnect — kept under 4 (PSI guideline). Only origins on the critical path. -->
22  <!-- api.cattive.com handles 100% of data + storage requests on every page.
23       O preconnect ao host cru do projeto (iefppahyskbnsaogvzem.supabase.co) foi
24       removido: o cliente usa VITE_SUPABASE_URL=api.cattive.com, então nada
25       conecta ao host cru — o hint era desperdício E expunha o ref do projeto. -->
26  <link rel="preconnect" href="https://api.cattive.com" crossorigin />
27  <!-- AgoraTicker external APIs (inmet, open-meteo, espn, brasilapi) were
28       previously dns-prefetched. Removed: AgoraBar mounts via
29       requestIdleCallback well after FCP, so the DNS lookup happens then
30       anyway. Six dns-prefetch entries also exceeded PSI's budget. -->
31  <link rel="dns-prefetch" href="https://www.googletagmanager.com" />
32  <link rel="dns-prefetch" href="https://www.google-analytics.com" />
33  <!-- Self-hosted Inter Variable (latin subset, ~47KB woff2) — inline @font-face +
34       preload means the font request fires immediately (parallel with CSS bundle)
35       and the swap is non-blocking. Previous `@fontsource-variable/inter` import
36       moved the @font-face into the render-blocking CSS chunk (PS
36I: FCP +600ms). -->
37  <link rel="preload" href="/fonts/inter-variable-latin.woff2" as="font" type="font/woff2" crossorigin />
38  <style>
39    @font-face{
40      font-family:'Inter';
41      font-style:normal;
42      font-weight:100 900;
43      font-display:swap;
44      src:url(/fonts/inter-variable-latin.woff2) format('woff2-variations');
45      unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
46    }
47  </style>
48  <style>
49    /* Edge above-the-fold skeleton (#app-shell, injected by
50       netlify/edge-functions/seo-meta-injection.ts). It is a SIBLING of #root,
51       overlaid above the (empty) #root, and removed once React paints real
52       content (removal observer near the end of <body>). Self-contained so it
53       renders correctly regardless of the main render-blocking CSS. */
54    #app-shell{position:absolute;top:0;left:0;right:0;z-index:30;background:#f8fafc;overflow:hidden}
55    #app-shell .sk-box{background:#e2e8f0;animation:sk-pulse 1.5s ease-in-out infinite}
56    #app-shell .sk-on-dark{background:rgba(255,255,255,.16)}
57    #app-shell .sk-cards{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}
58    @media(min-width:1024px){#app-shell .sk-cards{grid-template-columns:repeat(4,1fr)}}
59    @keyframes sk-pulse{0%,100%{opacity:1}50%{opacity:.5}}
60    @media(prefers-reduced-motion:reduce){#app-shell .sk-box{animation:none}}
61    /* ── Shell de ARTIGO (`#app-shell[data-shell="article"] .ea`) ──
62       Mesmo mecanismo do esqueleto de home (irmão de #root, removido pelo
63       observer no fim do <body>), mas o conteúdo é REAL: manchete, linha fina,
64       imagem e o corpo serializado pela edge (_shared/article-body.ts), para o
65       crawler que não executa JavaScript encontrar a matéria inteira.
66       Um leitor humano vê isto por alguns instantes, então tem de parecer
67       matéria: coluna de leitura, tipografia legível, imagem com caixa
68       reservada. Auto-contido de propósito — a tipografia canônica
69       (src/styles/articleProse.css) só chega no chunk lazy do ArticlePage, bem
70       depois deste primeiro paint. Valores espelham articleProse.css. */
71    #app-shell .ea{background:#fff;min-height:100vh;padding:20px 0 48px;font-family:'Inter',system-ui,-apple-system,sans-serif;-webkit-font-smoothing:antialiased}
72    #app-shell .ea-col{max-width:44rem;margin:0 auto;padding:0 20px}
73    #app-shell .ea-kicker{display:block;color:#c00;font-weight:700;font-size:.875rem;letter-spacing:.04em;text-transform:uppercase;margin:0 0 10px}
74    #app-shell .ea-title{font-size:2rem;line-height:1.15;font-weight:800;letter-spacing:-.02em;color:#0f172a;margin:0 0 12px}
75    #app-shell .ea-sub{font-size:1.25rem;line-height:1.5;color:#475569;margin:0 0 14px;font-weight:400}
76    #app-shell .ea-byline{font-size:.8125rem;color:#64748b;margin:0 0 20px}
77    #app-shell .ea-fig{max-width:60rem;margin:0 auto 24px;padding:0}
78    /* width/height no <img> + aspect-ratio aqui = caixa reservada antes do byte
79       da imagem chegar. Sem isso o corpo salta quando ela carrega (CLS). */
80    #app-shell .ea-img{display:block;width:100%;height:auto;aspect-ratio:16/9;-o-object-fit:cover;object-fit:cover;background:#e2e8f0}
81    #app-shell .ea-cap{font-size:.8125rem;color:#64748b;margin:8px 20px 0;line-height:1.5}
82    #app-shell .ea-prose{color:#334155;overflow-wrap:break-word}
83    #app-shell .ea-prose p{font-size:1.125rem;line-height:1.8;margin:0 0 1.25rem}
84    #app-shell .ea-prose h2{font-size:1.5rem;line-height:1.3;font-weight:700;color:#0f172a;margin:2rem 0 .75rem}
85    #app-shell .ea-prose h3{font-size:1.25rem;line-height:1.35;font-weight:700;color:#0f172a;margin:1.75rem 0 .625rem}
86    #app-shell .ea-prose h4,#app-shell .ea-prose h5,#app-shell .ea-prose h6{font-size:1.0625rem;font-weight:700;color:#0f172a;margin:1.5rem 0 .5rem}
87    #app-shell .ea-prose a{color:#1d4ed8;text-decoration:underline;text-underline-offset:2px}
88    #app-shell .ea-prose strong{font-weight:700;color:#1e293b}
89    #app-shell .ea-prose ul{list-style:disc;padding-left:1.5rem;margin:0 0 1.25rem}
90    #app-shell .ea-prose ol{list-style:decimal;padding-left:1.5rem;margin:0 0 1.25rem}
91    #app-shell .ea-prose li{font-size:1.125rem;line-height:1.75;margin:0 0 .5rem}
92    #app-shell .ea-prose blockquote{margin:1.5rem 0;padding:0 0 0 1.25rem;border-left:3px solid #c00;color:#475569;font-size:1.1875rem;line-height:1.7}
93    #app-shell .ea-prose figure{margin:1.5rem 0}
94    #app-shell .ea-prose img{display:block;max-width:100%;height:auto;margin:0 auto}
95    #app-shell .ea-prose figcaption{font-size:.8125rem;color:#64748b;margin-top:8px;line-height:1.5}
96    #app-shell .ea-prose hr{border:0;border-top:1px solid #e2e8f0;margin:2rem 0}
97    #app-shell .ea-prose table{width:100%;border-collapse:collapse;margin:1.5rem 0;font-size:.9375rem;display:block;overflow-x:auto}
98    #app-shell .ea-prose th,#app-shell .ea-prose td{border:1px solid #e2e8f0;padding:8px 10px;text-align:left}
99    #app-shell .ea-prose pre{background:#0f172a;color:#e2e8f0;padding:14px;border-radius:8px;overflow-x:auto;font-size:.875rem;margin:1.5rem 0}
100    @media(min-width:768px){
101      #app-shell .ea{padding:32px 0 64px}
102      #app-shell .ea-title{font-size:2.75rem}
103      #app-shell .ea-sub{font-size:1.375rem}
104    }
105  </style>
106
107  <!-- ══════════════════════════════════════════════════════════════════════
108       FOTO DE CAMPANHA — guarda de scripts de terceiro injetados PELA BORDA
109
110       🔴 POR QUE ISTO PRECISA SER INLINE, NO <head>, E NÃO PODE VIVER NO BUNDLE.
111       A zona do Cloudflare injeta o beacon de Web Analytics
112       (`
112<script type="module" src="static.cloudflareinsights.com/...">`) no fim
113       do <body> de todo documento HTML do domínio. Isso acontece DEPOIS do
114       Netlify (o Cloudflare é o proxy reverso na frente dele), então nenhuma
115       edge function nossa consegue removê-lo, e o bundle é um módulo diferido:
116       quando ele executa, o beacon já rodou. Só um script síncrono no topo do
117       <head> chega antes.
118
119       🔴 O QUE ELE FAZ, E O QUE COMPROVADAMENTE NÃO FAZ. Medido em produção em
120       15/08/2026 contra o beacon real:
121         • sem guarda: GET static.cloudflareinsights.com/beacon.min.js
122                     + POST fotodecampanha.com/cdn-cgi/rum   ← a COLETA
123         • com guarda: só o GET do arquivo. O POST não acontece.
124       Ou seja: o guarda impede a COLETA, que é o tratamento. O GET do arquivo
125       estático continua saindo (a busca já começou quando o observador acorda)
126       e não há como cancelá-la do cliente; ela vai para o mesmo CDN que já
127       termina o TLS de toda requisição deste domínio, então não revela a ele
128       nada que ele ainda não saiba.
129
130       🔴 OBSERVA `document`, NÃO `document.documentElement`. Este script roda
131       antes de o <html> existir em alguns caminhos de boot, e observar um nó
132       nulo falha em silêncio: o observador nunca dispara e o guarda vira
133       decoração. Custou uma rodada de medição descobrir isso.
134
135       🔴 NÃO É UM BLOQUEADOR GENÉRICO. A lista é fechada e só tem o que a
136       BORDA injeta sozinha. O que o nosso próprio código carrega (GA4, pixel)
137       já nasce atrás do consentimento em `medicao.ts` e não passa por aqui.
138
139       Espelho da chave: `CHAVE_DO_CONSENTIMENTO` em
140       src/features/foto-campanha/public/medicao/consentimento.ts.
141       ══════════════════════════════════════════════════════════════════════ -->
142  
142<script>
143    (function () {
144      var h = (location.hostname || '').toLowerCase();
145      if (h.indexOf('fotodecampanha.com') === -1 && h.indexOf('fotodecampanha.localhost') === -1) {
146        return;
147      }
148      // Só o que a borda injeta por conta própria.
149      var DA_BORDA = /cloudflareinsights\.com/i;
150      var CHAVE = 'fc_consentimento_v2';
151
152      // 🔴 ESTE NÚMERO ESPELHA `RegistroDeConsentimento.versao` DE
153      // `consentimento.ts`, E DIVERGIR QUEBRA NOS DOIS SENTIDOS. Medido em
154      // produção em 17/08/2026, quando a pergunta subiu de 2 para 3 e este
155      // literal ficou para trás:
156      //
157      //  • registro da versão VELHA: `lerConsentimento()` o descarta (a pessoa
158      //    volta ao estado "não decidiu"), mas este guarda o aceitava e
159      //    LIBERAVA o beacon da borda — terceiro rodando sem decisão válida,
160      //    que é exatamente o que este arquivo existe para impedir;
161      //  • registro da versão NOVA: o inverso e pior, porque é silencioso —
162      //    este guarda não reconheceria o aceite e reteria o beacon em todo
163      //    documento seguinte, para sempre, mesmo com a pessoa tendo aceitado.
164      //
165      // A paridade é travada por teste (`isolamentoDeMedicao.test.ts`): ele lê
166      // o número dos DOIS arquivos e exige igualdade. Subir a versão da
167      // pergunta é mudar os dois no mesmo commit.
168      var VERSAO_DA_PERGUNTA = 3;
169
170      var liberado = false;
171      try {
172        var bruto = localStorage.getItem(CHAVE);
173        if (bruto) {
174          var reg = JSON.parse(bruto);
175          liberado = reg && reg.versao === VERSAO_DA_PERGUNTA && reg.medicao === true;
176        }
177      } catch (e) {
178        /* storage indisponível: trata como não decidido, que é o lado seguro */
179      }
180
181      // Já consentiu com medição: a borda injeta e nada é retido.
182      window.__FC_BORDA_LIBERADA__ = liberado;
183      var retidos = (window.__FC_SCRIPTS_RETIDOS__ = []);
184      if (liberado) return;
185
186      function reter(no) {
187        if (!no || no.nodeName !== 'SCRIPT' || !no.getAttribute) return;
188        var src = no.getAttribute('src');
189        if (!src || !DA_BORDA.test(src)) return;
190        retidos.push({ src: src, dados: no.getAttribute('data-cf-beacon') || null });
191        // Os três de uma vez: sem src não há o que avaliar, o type inválido
192        // impede o navegador de tratá-lo como script, e a remoção tira o nó da
193        // lista de execução diferida dos módulos.
194        no.removeAttribute('src');
195        no.setAttribute('type', 'text/fc-retido');
196        if (no.parentNode) no.parentNode.removeChild(no);
197      }
198
199      var obs = new MutationObserver(function (lotes) {
200        for (var i = 0; i < lotes.length; i++) {
201          var novos = lotes[i].addedNodes;
202          for (var j = 0; j < novos.length; j++) reter(novos[j]);
203        }
204      });
205      obs.observe(document, { childList: true, subtree: true });
206
207      // Rede de segurança: o observador pode ser desconectado por engano numa
208      // refatoração futura, e uma varredura no fim do parse custa nada.
209      document.addEventListener('DOMContentLoaded', function () {
210        var restantes = document.querySelectorAll('script[src*="cloudflareinsights"]');
211        for (var k = 0; k < restantes.length; k++) reter(restantes[k]);
212      });
213
214      // Chamado por `medicao.ts` quando a pessoa aceita medição, para o que foi
215      // retido passar a valer sem obrigar a um recarregamento.
216      window.__FC_LIBERAR_BORDA__ = function () {
217        obs.disconnect();
218        window.__FC_BORDA_LIBERADA__ = true;
219        for (var i = 0; i < retidos.length; i++) {
220          var item = retidos[i];
221          if (document.querySelector('script[data-fc-liberado="' + item.src + '"]')) continue;
222          var s = document.createElement('script');
223          s.src = item.src;
224          s.defer = true;
225          s.setAttribute('data-fc-liberado', item.src);
226          if (item.dados) s.setAttribute('data-cf-beacon', item.dados);
227          document.head.appendChild(s);
228        }
229        retidos.length = 0;
230      };
231    })();
232  </script>
232
233
234  <!-- Speculation Rules API — Chrome 121+ pre-renders article-page links when
235       the user hovers or scrolls near them. Navigation to an article from the
236       home becomes instant (page already rendered in background). W3C spec,
237       Google-promoted — NOT cloaking. Eagerness "moderate" balances
238       prefetch cost vs hit rate. -->
239  
239<script type="speculationrules">
240    {
241      "prerender": [{
242        "where": {
243          "and": [
244            { "href_matches": "/noticias/*" },
245            { "not": { "href_matches": "/*/admin/*" } }
246          ]
247        },
248        "eagerness": "moderate"
249      }]
250    }
251  </script>
251
252
253  <!-- AI Discoverability & Agent-Ready -->
254  <link rel="manifest" href="/.well-known/ai-plugin.json" />
255  <link rel="author" href="/llms.txt" type="text/plain" />
256  <link rel="api-catalog" href="/.well-known/api-catalog" type="application/linkset+json" />
257  <link rel="service-desc" href="/openapi.yaml" type="application/yaml" />
258  <link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/feed.xml" />
259  
260  
260<script>
261    // Set title, description & favicon BEFORE React boots to prevent flash.
262    // Registry mirrors SITE_REGISTRY in src/utils/resolvePortalIdentity.ts.
263    // To add a new portal: add ONE entry here AND in resolvePortalIdentity.ts
264    // AND in netlify/edge-functions/_shared/strip-inline-registry.ts.
265    //
266    // ⚠️  PRIVACY / CROSS-TENANT INFO DISCLOSURE
267    // In production, the Netlify edge function `seo-meta-injection.ts`
268    // rewrites the inline portals registry array below to contain ONLY the
269    // matching entry for the request hostname (same for the GA registry).
270    // Without that strip, view-source on every apex exposes the full
271    // tenant list — fixed in PR addressing 2026-05-17 disclosure report.
272    // Localhost/dev keeps the full array (no edge function runs locally).
273    // Tests: netlify/edge-functions/_shared/__tests__/strip-inline-registry.test.ts
274    //
275    // 🚨 Do NOT write the literal token "var sites = [" or "var gaRegistry = {"
276    // anywhere except the real declarations below. The edge regex is anchored
277    // to line start (^\s*) for defense in depth, but it's still safer to
278    // avoid the literals in comments to prevent any future bypass risk.
279    (function () {
280      var h = location.hostname;
281      var p = location.pathname;
282      var isLocal = h === 'localhost' || h === '127.0.0.1' || h.indexOf('192.168.') === 0;
283
284      // Registry: [domainMatch[], localPaths[], title, description, favicon, shortName]
285      // The publicidade-legal entry MUST come first because the match below
286      // uses substring (hostname.indexOf), so a publicidade-legal subdomain
287      // of any parent tenant would otherwise inherit the parent's branding.
288      // Both the canonical no-hyphen prefix ('publicidadelegal.') and the legacy
289      // hyphen form ('publicidade-legal.') are listed so either matches first.
290      // See netlify/edge-functions/_shared/strip-inline-registry.ts.
291      var sites = [[["cbnamazonia"],["/cbn-amazonia"],"CBN Amazônia","CBN Amazônia - Seu portal regional de notícias com credibilidade.","https://api.cattive.com/storage/v1/object/public/news_assets/cbn-amazonia/ndrbimj7p9i_1775536554032.webp","CBN Amazônia"]];
292
293      // System routes (admin, console) — use Cattive branding
294      var systemPaths = ['/admin', '/console'];
295
296      var matched = null;
297
298      // 0. Prévias (<portal>.cattive.app) e demo antiga (demo-<portal>.cattive.com)
299      //    — o slug hifenizado não casa as keys de substring e 'cattive.com'
300      //    venceria. Casa pelo localPath ('/<slug>'). Mirror de findInlineEntry
301      //    em netlify/edge-functions/_shared/strip-inline-registry.ts e de
302      //    PORTAL_PREVIEW_HOST_RE / PORTAL_DEMO_HOST_RE em src/config/domains.ts
303      //    (sync!). Prévia de portal fora do registry: fica sem entrada e o
304      //    título/favicon padrão da página valem até o React.
305      var demoM = !isLocal && (h.match(/^demo-([a-z0-9-]+)\.cattive\.com$/) || h.match(/^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)\.cattive\.app$/));
306      if (demoM) {
307        for (var i = 0; i < sites.length; i++) {
308          for (var lp = 0; lp < sites[i][1].length; lp++) {
309            if (sites[i][1][lp] === '/' + demoM[1]) { matched = sites[i]; break; }
310          }
311          if (matched) break;
312        }
313      }
314
315      // 1. Production: match by domain
316      if (!isLocal && !matched) {
317        for (var i = 0; i < sites.length; i++) {
318          for (var d = 0; d < sites[i][0].length; d++) {
319            if (h.indexOf(sites[i][0][d]) !== -1) { matched = sites[i]; break; }
320          }
321          if (matched) break;
322        }
323      } else if (isLocal) {
324        // 2. Localhost: match by path prefix
325        for (var i = 0; i < sites.length; i++) {
326          for (var lp = 0; lp < sites[i][1].length; lp++) {
327            if (p === sites[i][1][lp] || p.indexOf(sites[i][1][lp] + '/') === 0) { matched = sites[i]; break; }
328          }
329          if (matched) break;
330        }
331        // 3. Localhost system routes → Cattive branding
332        if (!matched) {
333          for (var s = 0; s < systemPaths.length; s++) {
334            if (p.indexOf(systemPaths[s]) === 0) {
335              matched = [[], [], 'Cattive Platform', 'Cattive Platform', 'https://api.cattive.com/storage/v1/object/public/portal-assets/cattive/favicon.png', 'Cattive'];
336              break;
337            }
338          }
339        }
340      }
341
342      // 4. Fallback: localhost root → Cattive Platform, production unknown → JB
343      if (!matched) {
344        matched = isLocal
345          ? [[], [], 'Cattive Platform', 'Cattive Platform — Ambiente de Desenvolvimento Local', 'https://api.cattive.com/storage/v1/object/public/portal-assets/cattive/favicon.png', 'Cattive']
346          : [[], [], 'Cattive', 'Plataforma AI-First para Portais de Notícias', 'https://api.cattive.com/storage/v1/object/public/portal-assets/cattive/favicon.png', 'Cattive'];
347      }
348
349      // Apply
350      //
351      // ⚠️  NÃO sobrescrever title/description que já vieram preenchidos.
352      // `netlify/edge-functions/seo-meta-injection.ts` substitui a tag title e a
353      // meta description VAZIAS do topo deste arquivo pelos valores REAIS da
354      // página (título da matéria, meta
355      // description do artigo) antes do HTML sair do edge. Este script rodava
356      // logo depois e escrevia por cima o branding genérico do portal.
357      // Googlebot e Bingbot renderizam: se o bundle React demora, o snapshot
358      // que eles guardam é o genérico, e toda matéria do portal vira o mesmo
359      // título. Agora só preenche o que estiver VAZIO — em localhost e em host
360      // sem edge function os campos vêm vazios do arquivo e o comportamento
361      // antigo continua idêntico.
362      //
363      // 🔴 Não citar essas tags LITERALMENTE neste comentário. Ele sai no HTML
364      // servido de todo portal, e quem lê o HTML por expressão regular (a
365      // auditoria de 29/09/2026 e boa parte das ferramentas de SEO) contava a
366      // citação como segunda meta description vazia e segundo title. Travado
367      // em netlify/edge-functions/__tests__/meta-slots.test.ts.
368      var descEl = document.querySelector('meta[name="description"]');
369      var appleTitle = document.querySelector('meta[name="apple-mobile-web-app-title"]');
370      var appleTouchIcon = document.getElementById('apple-touch-icon');
371      var faviconEl = document.getElementById('favicon');
372      if (!document.title || !document.title.trim()) document.title = matched[2];
373      if (descEl && (!descEl.content || !descEl.content.trim())) descEl.content = matched[3];
374      if (faviconEl && !faviconEl.getAttribute('href')) faviconEl.href = matched[4];
375      if (appleTitle && (!appleTitle.content || !appleTitle.content.trim())) appleTitle.content = matched[5];
376      if (appleTouchIcon && !appleTouchIcon.getAttribute('href')) appleTouchIcon.href = matched[4];
377
378      // ── Google Analytics Boot (with Consent Mode v2) ──
379      // GA loads synchronously for Google's verification bot to detect.
380      // Consent Mode v2 ensures NO data is collected until user accepts cookies.
381      // Multi-tenant: cada portal tem a SUA propriedade, e todos mandam uma
382      // cópia para a propriedade de roll-up da plataforma (ver `gaPlataforma`).
383      // Admin/console subdomains are always excluded.
384      (function() {
385        // ── Roll-up da plataforma ──────────────────────────────────────────
386        // UMA propriedade GA4 que recebe uma cópia do tráfego de TODOS os
387        // sites, para a Cattive enxergar o conjunto sem depender de somar
388        // relatório por relatório. Ela NÃO substitui a propriedade do tenant:
389        // o gtag.js aceita vários `config` no mesmo documento e cada `config`
390        // é uma propriedade independente.
391        //
392        // 🔴 FORA DO REGISTRY DE TENANTS, E ISSO É O PONTO. A borda
393        // (`strip-inline-registry.ts`) reescreve aquele objeto para conter só
394        // a entrada do tenant servido — é o que impede o `view-source` de um
395        // apex de listar os outros clientes. O roll-up é o MESMO id para todo
396        // mundo, então não revela nada sobre a lista de tenants, e por isso
397        // vive numa var própria que a borda não toca. Se entrasse no registry,
398        // ou seria apagado pelo strip (roll-up morto em produção) ou o strip
399        // teria de preservá-lo — e aí a regra de "só a entrada do tenant"
400        // deixaria de ser verdadeira ao pé da letra.
401        //
402        // Valor reescrito no build por `vite-plugins/inject-ga-registry.ts` a
403        // partir de `inline-ga-registry.generated.json` (chave `platform`), a
404        // mesma fonte única do registry abaixo. Não editar só aqui.
405        var gaPlataforma = 'G-QXHDT8VS2D';
406
407        // Per-tenant GA4 measurement IDs. Hostname substring => measurement ID.
408        // Subdominios admin/console/auth/myaccount sao excluidos abaixo (sem
409        // tracking de back-office). O matching loop usa early break, entao
410        // cada tenant carrega APENAS o proprio GA, nunca cross-tenant.
411        // IMPORTANTE: ao adicionar uma chave aqui, atualize tambem
412        // netlify/edge-functions/_shared/strip-inline-registry.ts —
413        // a edge function reescreve este objeto para conter SOMENTE a
414        // entrada do tenant atual antes de servir o HTML.
415        var gaRegistry = {};
416
417        // Skip admin/console/auth/painel and demo-* subdomains (back-office e
418        // demos não poluem o GA de marketing do tenant/da Cattive — nem o
419        // roll-up, que é relatório de AUDIÊNCIA, não de uso de ferramenta)
420        var sub = h.split('.')[0];
421        if (sub === 'admin' || sub === 'console' || sub === 'auth' || sub === 'myaccount' || sub === 'painel' || sub.indexOf('demo-') === 0) return;
422
423        // Prévias de portal (<portal>.cattive.app): o cliente testando o próprio
424        // portal não é audiência de ninguém. ANTES do registry: a key 'cattive'
425        // casaria o host por substring e mediria a prévia na propriedade da Cattive.
426        if (h === 'cattive.app' || h.slice(-12) === '.cattive.app') return;
427
428        // Skip localhost
429        if (isLocal) return;
430
431        // 🔴 FOTO DE CAMPANHA NUNCA POR AQUI — nem o tenant, nem o roll-up.
432        // Aquele produto tem caminho próprio de medição
433        // (`src/features/foto-campanha/public/medicao/medicao.ts`), com
434        // consentimento por categoria em `fc_consentimento_v2` e o aviso do
435        // art. 11 na rota eleitoral. Este bloco decide por
436        // `portal_cookie_consent`, chave que aquele domínio NUNCA escreve:
437        // ligar o roll-up aqui mediria quem recusou e ignoraria quem aceitou,
438        // e o gtag entraria no documento antes de qualquer pergunta. O roll-up
439        // daquele produto entra por `ligarMedicao`, depois do "sim".
440        // Travado em `medicao/__tests__/isolamentoDeMedicao.test.ts`.
441        if (h.indexOf('fotodecampanha') !== -1 || h.indexOf('fotocampanha') !== -1) return;
442
443        // Deploy previews (*.netlify.app) não poluem propriedade real. O host
444        // de preview contém 'cattive' (cattiveportal.netlify.app) e casava a
445        // chave do registry por substring — cada preview virava sessão real.
446        if (h.indexOf('.netlify.app') !== -1) return;
447
448        // Find matching GA ID by domain
449        var gaId = null;
450        for (var key in gaRegistry) {
451          if (h.indexOf(key) !== -1) { gaId = gaRegistry[key]; break; }
452        }
453
454        // As propriedades DESTE documento: a do tenant (quando existe) mais a
455        // da plataforma, sem repetir. O dedupe importa: em cattive.com as duas
456        // são o mesmo id, e dois `config` iguais no mesmo documento dobram o
457        // page_view e inflam a sessão.
458        var gaIds = [];
459        if (gaId) gaIds.push(gaId);
460        if (gaPlataforma && gaIds.indexOf(gaPlataforma) === -1) gaIds.push(gaPlataforma);
461        if (!gaIds.length) return;
462
463        // Initialize gtag
464        window.dataLayer = window.dataLayer || [];
465        window.gtag = function() { window.dataLayer.push(arguments); };
466
467        // ── Consent Mode v2, modo AVANÇADO (LGPD-307: opt-in) ──
468        // O armazenamento continua opt-in: nada é gravado no aparelho de quem
469        // não aceitou, que é o que a LGPD-307 decidiu. O que muda é o ENVIO.
470        //
471        // 🔴 MODO AVANÇADO É ENVIAR O EVENTO SEMPRE, E ISSO NÃO É "MEDIR SEM
472        // CONSENTIMENTO". Com `analytics_storage: denied` o gtag.js manda o hit
473        // SEM cookie e SEM identificador — não há como reconhecer a pessoa, nem
474        // agora nem depois — e o GA4 usa esses hits para modelar quem recusou.
475        // Até 17/08/2026 o código fazia o contrário e ficava no pior par
476        // possível: baixava o gtag.js em TODA visita (o custo de rede e de
477        // terceiro já pago) e depois suprimia o evento, então não colhia nem o
478        // dado sem cookie nem a modelagem. Suprimir o envio não protege
479        // ninguém a mais do que o `denied` já protege; só apaga o número.
480        //
481        // ⚠️ A modelagem tem piso de volume (1.000 eventos/dia com `denied` por
482        // 7 dias, e 1.000 usuários/dia com `granted` em 7 dos últimos 28). Um
483        // portal sozinho dificilmente bate; a propriedade de roll-up, que soma
484        // todos, é onde isso passa a ser alcançável.
485        //
486        // 🔴 LÊ O GRANULAR PRIMEIRO. `portal_cookie_consent` é a chave GROSSA e
487        // vale 'all' só quando as TRÊS categorias estão ligadas — então quem
488        // aceitava estatística e recusava publicidade era gravado como
489        // 'essential' e, da segunda visita em diante, não era medido apesar de
490        // ter consentido. O mapa por categoria é a fonte precisa; a chave grossa
491        // fica como fallback de quem decidiu antes de ele existir.
492        var granular = null;
493        try { granular = JSON.parse(localStorage.getItem('portal_cookie_consent_granular') || 'null'); } catch(e) {}
494        var priorConsent = null;
495        try { priorConsent = localStorage.getItem('portal_cookie_consent'); } catch(e) {}
496        var aceitouTudo = priorConsent === 'all';
497        var okAnalytics = granular ? granular.analytics === true : aceitouTudo;
498        var okAnuncio = granular ? granular.advertising === true : aceitouTudo;
499        var okFuncional = granular ? granular.functionality === true : aceitouTudo;
500
501        // Set consent defaults BEFORE config (required by Google)
502        window.gtag('consent', 'default', {
503          analytics_storage: okAnalytics ? 'granted' : 'denied',
504          ad_storage: okAnuncio ? 'granted' : 'denied',
505          ad_user_data: okAnuncio ? 'granted' : 'denied',
506          ad_personalization: okAnuncio ? 'granted' : 'denied',
507          functionality_storage: okFuncional ? 'granted' : 'denied',
508          personalization_storage: okFuncional ? 'granted' : 'denied',
509          url_passthrough: true
510        });
511        // Sem consentimento de anúncio, o gtag redige os identificadores de
512        // publicidade que ainda assim iriam no hit. É o par recomendado do modo
513        // avançado: enviar, mas enviar limpo.
514        if (!okAnuncio) window.gtag('set', 'ads_data_redaction', true);
515
516        window.gtag('js', new Date());
517        // Rotas tokenizadas (ex.: /parceria/<token>) nunca podem chegar cruas
518        // ao GA — o token é bearer de acesso sem login. E /candidato/<nome>-<sq>
519        // carrega o NOME do candidato, que permite inferir opinião política
520        // (dado sensível, LGPD art. 11): o sq numérico fica, o nome sai.
521        // Espelho vanilla de src/lib/sanitizeTrackedPath.ts (manter em sync).
522        var safeBootPath = (location.pathname + location.search)
523          .replace(/^(\/parceria)\/[^/?#]+/i, '$1/:token')
524          .replace(/^(\/candidato)\/[^/?#]*?(\d{6,})(?=[/?#]|$)/i, '$1/:nome-$2')
525          .replace(/^(\/candidato)\/(?!:nome-)[^/?#]+/i, '$1/:handle')
526          .replace(/^(\/eleicoes\/candidato\/\d{4})\/[^/?#]*?-(\d+)(?=[/?#]|$)/i, '$1/:nome-$2');
527        // Dimensão de tenant do roll-up. O GA4 já coleta `hostname`, mas um
528        // portal pode ter mais de um host (apex, www, domínio legado) e o
529        // relatório do roll-up tem de somar os dois na mesma linha. O slug sai
530        // do localPath do registry, que é o id canônico do portal (o mesmo que
531        // `demo-<slug>.cattive.com` usa). Sem entrada casada, cai no hostname.
532        var portalSlug = (matched && matched[1] && matched[1][0]) ? matched[1][0].replace(/^\//, '') : h;
533        for (var gi = 0; gi < gaIds.length; gi++) {
534          window.gtag('config', gaIds[gi], {
535            // 🔴 SEMPRE `true`. Ver o bloco do modo avançado acima: quem decide
536            // se o hit leva cookie é o `analytics_storage`, não este campo.
537            send_page_view: true,
538            page_path: safeBootPath,
539            page_location: location.origin + safeBootPath,
540            portal: portalSlug
541          });
542        }
543
544        // Mark as injected for the React hook to detect.
545        // 🔴 `__GA_MEASUREMENT_ID` continua sendo SÓ o do tenant, e fica
546        // indefinido quando o tenant não tem propriedade própria. O hook usa
547        // essa ausência para ir buscar o id no Supabase — se o roll-up
548        // preenchesse esta chave, todo portal configurado só no banco perderia
549        // o GA próprio silenciosamente.
550        window.__GA_MEASUREMENT_ID = gaId || undefined;
551        window.__GA_PLATFORM_ID = gaPlataforma;
552        window.__GA_SEND_TO = gaIds;
553        window.__GA_CONSENT_GRANTED = okAnalytics;
554
555        // Um loader só serve todas as propriedades: o gtag.js é genérico e
556        // cada `config` acima já registrou a sua.
557        var s = document.createElement('script');
558        s.async = true;
559        s.src = 'https://www.googletagmanager.com/gtag/js?id=' + gaIds[0];
560        document.head.appendChild(s);
561      })();
562
563      // Generate dynamic Web Manifest for PWA
564      var isAdminPage = location.hostname.startsWith('admin.') || location.hostname.startsWith('console.') || location.pathname.startsWith('/admin') || location.pathname.startsWith('/console');
565      // Flash subdomain (`flash.<tenant>`) installs as its own PWA with the
566      // Flash branding so users see "Flash" on the home screen, not the
567      // generic portal name.
568      var isFlashSubdomain = location.hostname.startsWith('flash.');
569      var portalDisplayName = matched[2];
570      // O manifest descreve o APP (a marca do portal), não a página atual. Usa
571      // os valores do registry em vez de ler document.title / meta description:
572      // desde que o bloco acima passou a preservar o que o edge injetou, esses
573      // dois campos podem conter o título e a descrição de UMA matéria — e o
574      // PWA instalado ficaria com o nome de uma notícia na tela inicial.
575      // Resultado idêntico ao anterior, quando title e description eram sempre
576      // sobrescritos com matched[2] e matched[3].
577      var manifestName = isFlashSubdomain ? ('Flash · ' + portalDisplayName) : portalDisplayName;
578      var manifestShortName = isFlashSubdomain ? 'Flash' : (matched[5] || portalDisplayName);
579      var manifestDescription = isFlashSubdomain
580        ? ('Notícias em tela cheia do ' + portalDisplayName + ': clima, trânsito, cinema, eventos e mais.')
581        : matched[3];
582      // `.href` (não getAttribute) de propósito: resolve para URL absoluta, que
583      // é o que o manifest gerado como blob precisa — path relativo como
584      // '/favicon-sp.ico' não resolveria contra a origem do blob.
585      var logoSrc = document.getElementById('favicon').href;
586      var manifest = {
587        name: manifestName,
588        short_name: manifestShortName,
589        description: manifestDescription,
590        start_url: isAdminPage ? "/admin" : "/",
591        display: "standalone",
592        background_color: isFlashSubdomain ? "#000000" : "#ffffff",
593        theme_color: isFlashSubdomain
594          ? "#000000"
595          : (matched[2] === 'Cattive' || matched[2] === 'Cattive Platform') ? "#0f172a"
596            : matched[2] === 'Publicidade.Legal' ? "#1e293b"
597              : "#2563eb",
598        icons: [
599          {
600            src: logoSrc,
601            sizes: "192x192",
602            type: "image/png",
603            purpose: "any maskable"
604          },
605          {
606            src: logoSrc,
607            sizes: "512x512",
608            type: "image/png",
609            purpose: "any maskable"
610          }
611        ]
612      };
613
614      // Web Share Target API for Admin
615      if (isAdminPage) {
616        manifest.shortcuts = [
617          { name: "Novo Artigo", short_name: "Artigo", url: "/admin/posts/new", icons: [{ src: logoSrc, sizes: "192x192" }] },
618          { name: "Nova Imagem (DAM)", short_name: "Mídia", url: "/admin/media", icons: [{ src: logoSrc, sizes: "192x192" }] }
619        ];
620
621        manifest.share_target = {
622          action: "/admin/media",
623          method: "POST",
624          enctype: "multipart/form-data",
625          params: {
626            title: "title",
627            text: "text",
628            url: "url",
629            files: [
630              {
631                name: "file",
632                accept: ["image/jpeg", "image/png", "image/webp", "video/mp4"]
633              }
634            ]
635          }
636        };
637      }
638
639      // Encode manifest as Data URI to avoid CORS and Blob destruction issues
640      var manifestJson = encodeURIComponent(JSON.stringify(manifest));
641      var link = document.createElement('link');
642      link.rel = 'manifest';
643      link.href = 'data:application/manifest+json;charset=utf-8,' + manifestJson;
644      document.head.appendChild(link);
645    })();
646  </script>
646
647  
647<script type="module" crossorigin src="/assets/index-s_BTi5XF.js"></script>
647
648  <link rel="modulepreload" crossorigin href="/assets/vendor-react-BPk9u-w4.js">
649  <link rel="stylesheet" crossorigin href="/assets/index-GYMwT1An.css">
650  
650<script>(function(){try{var h=location.hostname.toLowerCase();var d=["fotodecampanha.com","fotodecampanha.com.br","cattive.com","cattive.me","publicidade.legal"],p=["painel","auth","admin","console","myaccount","business","anuncie","suporte","flash","tv","publicidade-legal"];for(var i=0;i<d.length;i++){if(h===d[i]||h==="www."+d[i])return;}for(var j=0;j<p.length;j++){if(h.indexOf(p[j]+".")===0)return;}var f=["/assets/PortalApp-CaflchGF.js","/assets/PortalHome-BjWw0OZK.js","/assets/vendor-icons-XRoHuOlC.js"];for(var k=0;k<f.length;k++){var l=document.createElement("link");l.rel="modulepreload";l.crossOrigin="";l.href=f[k];document.head.appendChild(l);}}catch(e){}})()</script>
650
651  <link rel="canonical" href="https://cbnamazonia.com/noticias" />
652  <link rel="alternate" hreflang="pt-BR" href="https://cbnamazonia.com/noticias" />
653  <link rel="alternate" hreflang="x-default" href="https://cbnamazonia.com/noticias" />
654  <meta property="og:title" content="Últimas Notícias - CBN Amazônia" />
655  <meta property="og:description" content="As últimas notícias de CBN Amazônia. Cobertura jornalística completa e atualizada sobre Amazônia Legal com credibilidade e agilidade." />
656  <meta property="og:url" content="https://cbnamazonia.com/noticias" />
657  <meta property="og:type" content="website" />
658  <meta property="og:site_name" content="CBN Amazônia" />
659  <meta property="og:locale" content="pt_BR" />
660  <meta property="og:image" content="https://cbnamazonia.com/.netlify/images?url=https%3A%2F%2Fcbnamazonia.com%2Fog%2Fcategory%2Fnoticias.svg&amp;w=1200&amp;h=630&amp;fit=cover&amp;fm=png&amp;q=90" />
661  <meta property="og:image:secure_url" content="https://cbnamazonia.com/.netlify/images?url=https%3A%2F%2Fcbnamazonia.com%2Fog%2Fcategory%2Fnoticias.svg&amp;w=1200&amp;h=630&amp;fit=cover&amp;fm=png&amp;q=90" />
662  <meta property="og:image:width" content="1200" />
663  <meta property="og:image:height" content="630" />
664  <meta property="og:image:alt" content="Últimas Notícias - CBN Amazônia" />
665  <meta name="twitter:card" content="summary_large_image" />
666  <meta name="twitter:title" content="Últimas Notícias - CBN Amazônia" />
667  <meta name="twitter:description" content="As últimas notícias de CBN Amazônia. Cobertura jornalística completa e atualizada sobre Amazônia Legal com credibilidade e agilidade." />
668  <meta name="twitter:image" content="https://cbnamazonia.com/.netlify/images?url=https%3A%2F%2Fcbnamazonia.com%2Fog%2Fcategory%2Fnoticias.svg&amp;w=1200&amp;h=630&amp;fit=cover&amp;fm=png&amp;q=90" />
669  <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
670  
670<script type="application/ld+json">[{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Início","item":"https://cbnamazonia.com"},{"@type":"ListItem","position":2,"name":"Últimas Notícias","item":"https://cbnamazonia.com/noticias"}]},{"@context":"https://schema.org","@type":"WebPage","@id":"https://cbnamazonia.com/noticias","name":"Últimas Notícias - CBN Amazônia","description":"As últimas notícias de CBN Amazônia. Cobertura jornalística completa e atualizada sobre Amazônia Legal com credibilidade e agilidade.","url":"https://cbnamazonia.com/noticias","isPartOf":{"@id":"https://cbnamazonia.com/#website"},"publisher":{"@id":"https://cbnamazonia.com/#organization"},"inLanguage":"pt-BR"}]</script>
670
671</head>
672
673<body class="bg-slate-50 text-slate-900 antialiased selection:bg-blue-100 selection:text-blue-900"><h1 style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0">Últimas Notícias - CBN Amazônia</h1>
674  <!-- #A11 — Skip Navigation Link
675       Inline style removed: Tailwind `sr-only` already hides the link until focused,
676       and the inline style was overriding `focus:not-sr-only` (PSI: "skip link not focusable"). -->
677  <a href="#main-content" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[9999] focus:bg-blue-600 focus:text-white focus:px-4 focus:py-2 focus:rounded-lg focus:text-sm focus:font-medium focus:shadow-lg">Pular para o conteúdo</a>
678  <div id="global-admin-preloader-container"></div>
679  <div id="root"></div>
680  <!-- Crawler / no-JS fallback.
681       Bots that don't execute JS (or pause before React mounts) used to see
682       "JavaScript Required" here and surface it as the page description in
683       Google Alerts / WhatsApp / FB previews. We now render a semantic
684       fallback that mirrors the page <title> and <meta description>
685       (populated either by the inline boot script above or by the
686       seo-meta-injection edge function). Discoverability links point
687       crawlers to the structured feeds. The whole block is hidden when JS
688       runs (React replaces #root), so end users never see it.
689       —2026-05-18 SEO incident fix. -->
690  <noscript>
691    <article style="max-width:720px;margin:60px auto;padding:24px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.5;color:#0f172a">
692      <h1 id="ns-title" style="font-size:32px;font-weight:700;margin:0 0 12px;line-height:1.2">Últimas Notícias - CBN Amazônia</h1>
693      <p id="ns-desc" style="font-size:18px;margin:0 0 24px;color:#475569">As últimas notícias de CBN Amazônia. Cobertura jornalística completa e atualizada sobre Amazônia Legal com credibilidade e agilidade.</p>
694      <nav aria-label="Recursos para crawlers">
695        <h2 style="font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#64748b;margin:0 0 8px">Recursos</h2>
696        <ul style="list-style:none;padding:0;margin:0;display:flex;flex-wrap:wrap;gap:12px">
697          <li><a href="/feed.xml" style="color:#2563eb;text-decoration:underline">RSS Feed</a></li>
698          <li><a href="/sitemap.xml" style="color:#2563eb;text-decoration:underline">Mapa do site</a></li>
699          <li><a href="/llms.txt" style="color:#2563eb;text-decoration:underline">AI agents (llms.txt)</a></li>
700          <li><a href="/noticias" style="color:#2563eb;text-decoration:underline">Últimas notícias</a></li>
701        </ul>
702      </nav>
703      <p style="margin:32px 0 0;padding-top:16px;border-top:1px solid #e2e8f0;color:#94a3b8;font-size:14px">Para a experiência completa, habilite JavaScript no seu navegador.</p>
704    </article>
705  </noscript>
706  <!-- Platform Health: Blank Screen Auto-Recovery V4
707       - Robô (navigator.webdriver ou UA de robô): nunca recarrega nem mostra aviso; só registra
708       - App vivo (#root com filhos ou window.__APP_BOOT) sem falha provada: não recarrega;
709         aos 15 s visíveis registra slow_boot e mostra um aviso discreto com "Recarregar"
710         (aos 30 s se o esqueleto da borda está na tela)
711       - Falha provada: SCRIPT/LINK de /assets/ ou chunk que falhou, ou #root SEM filhos
712         depois do DOMContentLoaded (entrada que não rodou, ou erro JS do próprio host que
713         derrubou a árvore). Recarrega, 1ª simples, 2ª com limpeza só com evidência de
714         service worker; a mesma falha depois de uma recarga vai para o overlay (com o app
715         montado, para o aviso)
716       - Erro de módulo acessório com o app montado não é queda; promessa rejeitada só conta
717         se for chunk; SyntaxError e erro de ICU/Intl nunca recarregam; "Script error.", erro
718         de outro host e de script inline não contam
719       - Navegação SPA desde a carga: nunca recarrega (#3198)
720       - Aba oculta: só conta o tempo visível
721       - Sends beacon to log-incident for every detected incident -->
722  
722<script>
723    (function() {
724      // Todo erro visto, para diagnóstico (beacon e overlay). Só `ownErrors`,
725      // `resErrors` e `fatalError` pesam na decisão.
726      var errors = [];
727      var ownErrors = [];
728      var resErrors = [];
729      var fatalError = null;
730      var RELOAD_KEY = '_pf_recovery_count';
731      var RELOAD_TS_KEY = '_pf_recovery_ts';
732      var SIG_KEY = '_pf_recovery_sig';
733      var SID_KEY = '_pf_sid';
734      var MAX_RETRIES = 2;
735      var SESSION_WINDOW_MS = 60000; // Reset counter after 60s of no issues
736
737      // Caminho da carga. Uma navegação SPA para rota lazy deixa o #root sem
738      // texto enquanto o Suspense espera o chunk; se isso coincide com uma
739      // checagem, o boot NÃO falhou. Visto em produção em 28/09/2026: SPA
740      // /empresas → /guia-educacao aos ~10 s, recarga aos 12 s, e a limpeza
741      // "nuclear" na segunda. Com navegação, o app está vivo por definição e
742      // nunca é recarregado daqui (ver decideBlankAction).
743      var BOOT_PATH = location.pathname;
744
745      // Relógio. Tudo é contado em tempo VISÍVEL desde o parse deste script:
746      // aba aberta em segundo plano não é boot lento (o navegador estrangula
747      // timers e rede de aba oculta).
748      var FIRST_CHECK_MS = 5000;
749      var RECHECK_MS = 3500;       // 5 s, 8,5 s, 12 s, 15,5 s...
750      var OVERLAY_MIN_MS = 12000;  // overlay nunca antes dos 12 s
751      var SLOW_NOTICE_MS = 15000;  // slow_boot no beacon e aviso "demorando" para app vivo
752      // Com o esqueleto da borda na tela o leitor já vê as manchetes: o aviso
753      // espera mais (no Slow 4G com CPU 4x ele aparecia em 6 de 6 cargas por
754      // cima do esqueleto, e "Recarregar" reiniciava um boot de 40 s).
755      var SLOW_NOTICE_SHELL_MS = 30000;
756      var DCL_GRACE_MS = 2000;     // React leva um instante entre a entrada rodar e o 1º commit
757      var WATCH_MAX_MS = 180000;   // depois disso o detector para de olhar
758      var START = Date.now();
759      var recovering = false;
760      var finished = false;
761
762      // Robô não é leitor: recarregar ou cobrir a tela de um renderizador de
763      // busca só atrapalha (e cerca de 90% dos incidentes do JB em 29/09/2026
764      // eram robô). `navigator.webdriver` pega Playwright/Selenium/Puppeteer. A
765      // lista NÃO usa /render/ solto. "cubot" é marca de celular Android, não
766      // robô. Google-InspectionTool (inspeção de URL do Search Console),
767      // BingPreview e Chrome-Lighthouse (PageSpeed) não têm "bot" no UA.
768      var UA = String(navigator.userAgent || '');
769      var IS_BOT = navigator.webdriver === true ||
770        (/bot|spider|crawl|slurp|headless|googlebot|bingbot|yandex|baiduspider|duckduckbot|facebookexternalhit|petalbot|ahrefs|semrush|inspectiontool|bingpreview|lighthouse/i.test(UA) &&
771          !/cubot/i.test(UA));
772
773      // Estado entre recargas. Com o sessionStorage bloqueado (SecurityError,
774      // 51 eventos em 6 dias no Sentry), o contador voltava a 0 a cada carga e
775      // a entrada morta recarregava em loop, sem teto. Reserva: window.name,
776      // que sobrevive à recarga na mesma aba, usado só quando está vazio ou já
777      // é nosso. Sem nenhum dos dois não há teto confiável, e o detector não
778      // recarrega sozinho (ver getReloadCount).
779      var NAME_PREFIX = '__pf:';
780      function nameState() {
781        try {
782          var n = String(window.name || '');
783          if (n === '') return {};
784          if (n.indexOf(NAME_PREFIX) === 0) return JSON.parse(n.slice(NAME_PREFIX.length)) || {};
785        } catch(e) {}
786        return null;
787      }
788      function nameWrite(o) {
789        var empty = true;
790        for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) { empty = false; break; }
791        try { window.name = empty ? '' : NAME_PREFIX + JSON.stringify(o); } catch(e) {}
792      }
793      function sGet(k) {
794        try { return sessionStorage.getItem(k); } catch(e) {}
795        var o = nameState();
796        return o && Object.prototype.hasOwnProperty.call(o, k) ? String(o[k]) : null;
797      }
798      function sSet(k, v) {
799        try { sessionStorage.setItem(k, v); return; } catch(e) {}
800        var o = nameState();
801        if (o) { o[k] = String(v); nameWrite(o); }
802      }
803      function sDel(k) {
804        try { sessionStorage.removeItem(k); return; } catch(e) {}
805        var o = nameState();
806        if (o && Object.prototype.hasOwnProperty.call(o, k)) { delete o[k]; nameWrite(o); }
807      }
808      var CAN_PERSIST = (function() {
809        try { sessionStorage.setItem('_pf_probe', '1'); sessionStorage.removeItem('_pf_probe'); return true; } catch(e) {}
810        return nameState() !== null;
811      })();
812
813      // Destino do beacon. O caminho relativo dava 404 no domínio do portal (o
814      // netlify.toml não encaminha /functions/v1): até 28/09/2026 a tabela
815      // platform_incidents nunca tinha recebido uma linha de produção, só de
816      // localhost. O Vite troca o marcador pela VITE_SUPABASE_URL no build (em
817      // produção, https://api.cattive.com, o mesmo host do resto do frontend;
818      // o proxy preserva o IP do leitor, que o rate limit da função usa). Build
819      // sem a variável deixa o marcador cru, e aí fica o relativo de antes.
820      var INCIDENT_URL = (function() {
821        var base = 'https://api.cattive.com';
822        if (!/^https?:\/\/[^%\s]+$/.test(base)) return '/functions/v1/log-incident';
823        return base.replace(/\/+$/, '') + '/functions/v1/log-incident';
824      })();
825
826      // Id da aba (sessionStorage), para ligar recarga → carga seguinte →
827      // overlay no platform_incidents. Aleatório, sem nada do leitor.
828      var SESSION_ID = (function() {
829        var id = sGet(SID_KEY) || '';
830        if (!id) {
831          id = Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
832          sSet(SID_KEY, id);
833        }
834        return id;
835      })();
836
837      window.__diag = { errors: errors, phase: 'init' };
838      window.__platformHealth = { incidents: [] };
839
840      function perfNow() {
841        try { return Math.round(performance.now()); } catch(e) { return Date.now() - START; }
842      }
843
844      // DOMContentLoaded só dispara depois que a entrada (módulo, que é defer)
845      // e os imports estáticos dela executaram ou falharam. "DCL disparou e
846      // nada do app" prova que a entrada morreu; "DCL ainda não disparou"
847      // prova que ainda está baixando. document.readyState==='c
847omplete' não
848      // serve: espera imagens e anúncio.
849      var dclAt = document.readyState === 'loading' ? null : Date.now();
850      document.addEventListener('DOMContentLoaded', function() { if (dclAt === null) dclAt = Date.now(); });
851
852      var ORIGIN = String(location.origin || '');
853      function isSameOrigin(u) {
854        u = String(u || '');
855        return u.charAt(0) === '/' || (ORIGIN !== '' && u.indexOf(ORIGIN + '/') === 0);
856      }
857      function pathOf(u) {
858        u = String(u || '');
859        return ORIGIN !== '' && u.indexOf(ORIGIN) === 0 ? u.slice(ORIGIN.length) : u;
860      }
861      function isOwnAsset(u) { return isSameOrigin(u) && pathOf(u).indexOf('/assets/') === 0; }
862
863      var CHUNK_RE = /Failed to fetch dynamically imported module|Importing a module script failed|error loading dynamically imported module|Unable to preload CSS|is not a valid JavaScript MIME type|Expected a JavaScript(?:-or-Wasm)? module script/i;
864      // HTML servido no lugar de JS: o service worker velho (ou um cache de
865      // borda) devolvendo index.html em /assets/*.js.
866      var MIME_RE = /is not a valid JavaScript MIME type|Expected a JavaScript(?:-or-Wasm)? module script but the server responded with a MIME type of ["']?text\/html|Unexpected token\s*'?</i;
867
868      // Erro que recarregar não conserta: sintaxe que o navegador não entende
869      // (navegador antigo) e ICU/Intl quebrado. A exceção é "Unexpected token
870      // '<'", que é HTML no lugar de JS e aí a recarga (ou a limpeza) resolve.
871      function isFatal(msg, err) {
872        if (/Unexpected token\s*'?</i.test(msg)) return false;
873        if ((err && err.name === 'SyntaxError') || /SyntaxError|Unexpected token|Invalid regular expression/i.test(msg)) return true;
874        return /\bICU\b|Icu error|\bIntl\b|Incorrect locale information|Invalid language tag|Invalid time zone/i.test(msg);
875      }
876
877      function noteError(kind, msg, file, line, err) {
878        msg = String(msg || (err && err.message) || '');
879        file = String(file || '');
880        var entry = kind === 'ERR'
881          ? '[ERR] ' + msg + ' @ ' + (file.split('/').pop() || '?') + ':' + (line || '?')
882          : '[REJ] ' + msg.substring(0, 200);
883        errors.push(entry);
884        // "Script error." é erro de script de outro host sem CORS: nada a ver
885        // com o boot. Idem para erro com arquivo de outro host (anúncio,
886        // métrica, extensão do navegador).
887        if (/^\s*(Uncaught\s+)?Script error\.?\s*$/i.test(msg)) return;
888        if (file && !isSameOrigin(file)) return;
889        // Script inline (arquivo = a própria página): GA, anúncio, estes
890        // scripts de boot. Quebrar um deles não impede a entrada de rodar.
891        if (file && file.split('#')[0].split('?')[0] === ORIGIN + location.pathname) return;
892        // Promessa rejeitada à toa (fetch que falhou, AbortError, JSON.parse de
893        // resposta ruim, que é SyntaxError) não prova que o boot caiu; import
894        // dinâmico que falhou prova. Este filtro vem ANTES do isFatal: um
895        // "Unexpected end of JSON input" rejeitado virava erro fatal e cobria
896        // com o overlay um app vivo e só lento.
897        if (kind === 'REJ' && !CHUNK_RE.test(msg)) return;
898        if (kind === 'ERR' && !file && !CHUNK_RE.test(msg) &&
899            !(err && err.stack && ORIGIN !== '' && String(err.stack).indexOf(ORIGIN) >= 0)) return;
900        if (isFatal(msg, err)) { if (!fatalError) fatalError = entry; return; }
901        ownErrors.push(entry);
902      }
903
904      // Fase de captura: é o único jeito de ver a falha de 
904<script>/<link>
905      // (404, bloqueio, MIME), porque erro de recurso não borbulha até o
906      // window. Antes disso, entrada 404 deixava o #root vazio com errors=[].
907      window.addEventListener('error', function(e) {
908        var t = e && e.target;
909        if (t && t !== window && t.tagName) {
910          var tag = String(t.tagName).toUpperCase();
911          var rel = String((t.getAttribute && t.getAttribute('rel')) || '').toLowerCase();
912          var url = t.src || t.href || '';
913          if ((tag === 'SCRIPT' || (tag === 'LINK' && /stylesheet|modulepreload/.test(rel))) && isOwnAsset(url)) {
914            resErrors.push(pathOf(url));
915            errors.push('[RES] ' + tag.toLowerCase() + ' ' + pathOf(url).substring(0, 160));
916          }
917          return;
918        }
919        noteError('ERR', e && e.message, e && e.filename, e && e.lineno, e && e.error);
920      }, true);
921      window.addEventListener('unhandledrejection', function(e) {
922        var r = e && e.reason;
923        noteError('REJ', r && r.message ? r.message : String(r), '', null, r);
924      });
925
926      // Get reload attempt count (with stale window check)
927      // Sem onde guardar o contador, conta como tentativas esgotadas: melhor o
928      // overlay com o botão do que recarregar em loop.
929      function getReloadCount() {
930        if (!CAN_PERSIST) return MAX_RETRIES;
931        var ts = parseInt(sGet(RELOAD_TS_KEY) || '0', 10);
932        if (Date.now() - ts > SESSION_WINDOW_MS) {
933          sDel(RELOAD_KEY);
934          sDel(RELOAD_TS_KEY);
935          return 0;
936        }
937        return parseInt(sGet(RELOAD_KEY) || '0', 10) || 0;
938      }
939
940      function incrementReloadCount() {
941        var count = getReloadCount() + 1;
942        sSet(RELOAD_KEY, count.toString());
943        sSet(RELOAD_TS_KEY, Date.now().toString());
944        return count;
945      }
946
947      function clearReloadCount() {
948        sDel(RELOAD_KEY);
949        sDel(RELOAD_TS_KEY);
950        sDel(SIG_KEY);
951      }
952
953      // Assinatura da falha, sem hash de build, query nem linha: a mesma falha
954      // que sobrevive a uma recarga é determinística (bloqueio, navegador,
955      // robô) e recarregar de novo só repete o branco.
956      function normalizeSig(s) {
957        return String(s)
958          .replace(/[?#][^\s'"]*/g, '')
959          .replace(/-[A-Za-z0-9_-]{8}\.(js|css)\b/g, '.$1')
960          .replace(/:\d+(:\d+)?/g, '')
961          .substring(0, 160);
962      }
963      function currentSig() {
964        if (fatalError) return 'fatal:' + normalizeSig(fatalError);
965        if (resErrors.length) return 'res:' + normalizeSig(resErrors[0]);
966        if (ownErrors.length) return 'err:' + normalizeSig(ownErrors[0]);
967        return 'entry_dead';
968      }
969      function readLastReload() {
970        try {
971          var o = JSON.parse(sGet(SIG_KEY) || 'null');
972          if (!o || Date.now() - (o.ts || 0) > SESSION_WINDOW_MS) return null;
973          return o;
974        } catch(e) { return null; }
975      }
976      function writeLastReload(sig, method) {
977        sSet(SIG_KEY, JSON.stringify({ sig: sig, method: method, ts: Date.now() }));
978      }
979
980      // O src/lib/chunkErrorRecovery.ts recarrega sozinho em erro de chunk
981      // (chave `_chunk_reload_ts`). Marca desta carga: ele está recarregando
982      // agora, não disputar. Marca da carga anterior (até 60 s): a falha de
983      // chunk de agora já sobreviveu a uma recarga.
984      function chunkReloadTs() {
985        try { return parseInt(sessionStorage.getItem('_chunk_reload_ts') || '0', 10) || 0; } catch(e) { return 0; }
986      }
987
988      function hasSwController() {
989        try { return !!(navigator.serviceWorker && navigator.serviceWorker.controller); } catch(e) { return false; }
990      }
991
992      // Limpeza "nuclear": o caso que ela resolve é o service worker velho
993      // servindo HTML ou chunk 404 em /assets/. Só roda com evidência (ver
994      // decideBlankAction) e apaga só os caches do SW que guardam página ou
995      // JS. Imagens do flipbook/Flash e fontes ficam. Desregistrar o SW derruba
996      // a inscrição de push do leitor, então só acontece com a assinatura de
997      // HTML servido como JS: aí o próprio fetch do SW velho é o defeito e
998      // apagar cache não basta (mesma regra do chunkErrorRecovery).
999      function nuclearEscape(unregister, done) {
1000        var called = false;
1001        function fin() { if (called) return; called = true; try { done(); } catch(e) {} }
1002        try {
1003          var tasks = [];
1004          if (unregister && navigator.serviceWorker && navigator.serviceWorker.getRegistrations) {
1005            tasks.push(navigator.serviceWorker.getRegistrations()
1006              .then(function(rs){ return Promise.all(rs.map(function(r){ return r.unregister(); })); })
1007              .catch(function(){}));
1008          }
1009          if (typeof caches !== 'undefined') {
1010            tasks.push(caches.keys()
1011              .then(function(ks){
1012                return Promise.all(ks.filter(function(k){
1013                  return !/^(flipbook-images|fonts|flash-images|flash-weather)/.test(k);
1014                }).map(function(k){ return caches.delete(k); }));
1015              })
1016              .catch(function(){}));
1017          }
1018          Promise.all(tasks).then(fin, fin);
1019        } catch(e) { fin(); }
1020        // Never hang the recovery on a wedged CacheStorage/SW call.
1021        setTimeout(fin, 1200);
1022      }
1023
1024      // Só erro do app: um JSON.parse rejeitado com "Unexpected token '<'"
1025      // (API devolvendo HTML) não é HTML servido no lugar de JS.
1026      function hasMimeEvidence() {
1027        for (var i = 0; i < ownErrors.length; i++) if (MIME_RE.test(ownErrors[i])) return true;
1028        return false;
1029      }
1030      // Falha de ativo de /assets/ ou de chunk: prova de boot quebrado mesmo com
1031      // o app montado (é a rota que não vem).
1032      function hasHardError() {
1033        if (resErrors.length) return true;
1034        for (var i = 0; i < ownErrors.length; i++) if (CHUNK_RE.test(ownErrors[i])) return true;
1035        return false;
1036      }
1037
1038      function recoverAndReload(metrics, phase, method, sig) {
1039        recovering = true;
1040        incrementReloadCount();
1041        writeLastReload(sig, method);
1042        logIncident('blank_screen', phase, metrics, true, method === 'nuclear' ? 'nuclear_reload' : 'auto_reload', { sig: sig });
1043        var go = function() { location.reload(); };
1044        if (method === 'nuclear') { nuclearEscape(hasMimeEvidence(), function() { setTimeout(go, 150); }); }
1045        else { setTimeout(go, 150); }
1046      }
1047
1048      // Manual recovery from the overlay button: o leitor já passou das
1049      // tentativas automáticas, então limpa os caches do SW antes.
1050      window.__pfNuclearReload = function() {
1051        clearReloadCount();
1052        nuclearEscape(hasMimeEvidence(), function() { location.reload(); });
1053        setTimeout(function() { location.reload(); }, 1300);
1054      };
1055
1056      // Texto VISÍVEL do #root: sem <style>/
1056<script>/<noscript>/<template>. O
1057      // GlobalAudioPlayer (montado fora da rota) injeta um <style> de 749
1058      // caracteres de @keyframes, e o textContent contava isso como conteúdo.
1059      function visibleTextLength(root) {
1060        var n = 0;
1061        try {
1062          var walker = document.createTreeWalker(root, 4 /* NodeFilter.SHOW_TEXT */, null);
1063          var node;
1064          while ((node = walker.nextNode())) {
1065            var p = node.parentNode;
1066            var skip = false;
1067            while (p && p !== root) {
1068              var tag = p.nodeName;
1069              if (tag === 'STYLE' || tag === 'SCRIPT' || tag === 'NOSCRIPT' || tag === 'TEMPLATE') { skip = true; break; }
1070              p = p.parentNode;
1071            }
1072            if (!skip) n += String(node.nodeValue || '').trim().length;
1073          }
1074        } catch(e) { n = (root.textContent || '').trim().length; }
1075        return n;
1076      }
1077
1078      // DOM visibility check. O esqueleto da borda (#app-shell) é IRMÃO do
1079      // #root, então não entra aqui: o #root só tem conteúdo quando o React
1080      // pinta.
1081      function checkBlank() {
1082        var root = document.getElementById('root');
1083        if (!root) return { isBlank: true, info: 'NO_ROOT', metrics: {} };
1084        var h = root.scrollHeight;
1085        var kids = root.children.length;
1086        var txt = visibleTextLength(root);
1087        var imgs = root.querySelectorAll('img').length;
1088        var buttons = root.querySelectorAll('button,a').length;
1089        var visEls = root.querySelectorAll('header,nav,main,article,section,h1,h2,h3,p').length;
1090        var isBlank = (txt < 10 && imgs === 0 && buttons < 2 && visEls < 2);
1091        return {
1092          isBlank: isBlank,
1093          info: 'scrollH=' + h + ', children=' + kids + ', text=' + txt + ', imgs=' + imgs + ', btns=' + buttons + ', semantic=' + visEls,
1094          metrics: { scrollHeight: h, children: kids, text: txt, imgs: imgs, btns: buttons, semantic: visEls }
1095        };
1096      }
1097      // Para o bloco do #app-shell: a rede de segurança dele não tira o
1098      // esqueleto enquanto o #root estiver em branco.
1099      window.__pfRootIsBlank = function() { return checkBlank().isBlank; };
1100
1101      // Walk DOM tree for diagnostics
1102      function walkDOM(root) {
1103        try {
1104          var walk = function(el, depth) {
1105            if (depth > 4 || !el) return '';
1106            var tag = el.tagName ? el.tagName.toLowerCase() : '#text';
1107            var cls = el.className ? (' class="' + String(el.className).substring(0, 60) + '"') : '';
1108            var s = new Array(depth + 1).join('  ') + '<' + tag + cls + '>\n';
1109            for (var i = 0; i < Math.min(el.children.length, 5); i++) {
1110              s += walk(el.children[i], depth + 1);
1111            }
1112            return s;
1113          };
1114          return walk(root, 0);
1115        } catch(e) { return 'walk error: ' + e.message; }
1116      }
1117
1118      // Rotas tokenizadas (/parceria/<token>) nunca podem ser persistidas
1119      // cruas: o token é bearer de acesso sem login e o DB só guarda o
1120      // SHA-256 dele. Mesma coisa para /candidato/<nome>-<sq>: o nome do
1121      // candidato permite inferir opinião política (dado sensível, LGPD
1122      // art. 11), então o sq numérico fica e o nome sai. Espelho vanilla de
1123      // src/lib/sanitizeTrackedPath.ts (manter em sync), mesmos regexes do
1124      // boot do GA acima.
1125      function safePath(p) {
1126        return String(p)
1127          .replace(/^(\/parceria)\/[^/?#]+/i, '$1/:token')
1128          .replace(/^(\/candidato)\/[^/?#]*?(\d{6,})(?=[/?#]|$)/i, '$1/:nome-$2')
1129          .replace(/^(\/candidato)\/(?!:nome-)[^/?#]+/i, '$1/:handle')
1130          .replace(/^(\/eleicoes\/candidato\/\d{4})\/[^/?#]*?-(\d+)(?=[/?#]|$)/i, '$1/:nome-$2');
1131      }
1132
1133      function appBootMs() {
1134        var b = window.__APP_BOOT;
1135        return typeof b === 'number' ? Math.round(b) : null;
1136      }
1137
1138      // Send incident to logging edge function via beacon (fire-and-forget)
1139      function logIncident(type, phase, metrics, autoResolved, resolutionMethod, extra) {
1140        try {
1141          var root = document.getElementById('root');
1142          var meta = {
1143            phase: phase,
1144            boot_path: safePath(BOOT_PATH),
1145            metrics: metrics,
1146            errors: errors.slice(0, 10),
1147            dom_tree: type === 'blank_screen' ? walkDOM(root) : null,
1148            reload_attempts: getReloadCount(),
1149            timestamp: new Date().toISOString(),
1150            ua_class: IS_BOT ? 'bot' : 'human',
1151            session_id: SESSION_ID,
1152            t_ms: perfNow(),
1153            visible_ms: Math.round(visibleElapsed()),
1154            dcl: dclAt !== null,
1155            app_boot: appBootMs() === null ? false : appBootMs(),
1156            app_mounted: !!(root && root.children.length > 0),
1157            shell_present: !!document.getElementById('app-shell'),
1158            res_errors: resErrors.slice(0, 5)
1159          };
1160          if (extra) for (var k in extra) if (Object.prototype.hasOwnProperty.call(extra, k)) meta[k] = extra[k];
1161          var payload = JSON.stringify({
1162            incident_type: type,
1163            severity: autoResolved ? 'medium' : 'critical',
1164            url: location.origin + safePath(location.pathname + location.search),
1165            path: safePath(location.pathname),
1166            user_agent: navigator.userAgent,
1167            error_message: fatalError || ownErrors[0] || errors[0] || null,
1168            auto_resolved: autoResolved,
1169            resolution_method: resolutionMethod,
1170            session_id: SESSION_ID,
1171            metadata: meta
1172          });
1173          // Use sendBeacon for reliability (survives page unload/reload
1173)
1174          if (navigator.sendBeacon) {
1175            navigator.sendBeacon(INCIDENT_URL, payload);
1176          }
1177          // Also store locally for the health service to pick up
1178          window.__platformHealth.incidents.push(JSON.parse(payload));
1179        } catch(e) { /* silent */ }
1180      }
1181
1182      // O que fazer com uma checagem. Pura: o teste roda este bloco no jsdom
1183      // (src/config/__tests__/blankScreenWatchdog.test.ts).
1184      //   'ok'       → #root com conteúdo visível: zera o contador de recargas.
1185      //   'wait'     → app vivo ou entrada ainda baixando, sem falha provada.
1186      //                Página lenta não é página quebrada: recarregar joga
1187      //                fora o download em andamento e mostra outro branco.
1188      //   'spa_wait' → idem, com navegação SPA desde a carga (#3198).
1189      //   'slow'     → 'wait' passado dos 15 s visíveis: aviso discreto com
1190      //                "Recarregar" e slow_boot no beacon, sem recarga.
1191      //   'recover'  → falha provada, recarga simples. Falha provada é:
1192      //                - SCRIPT/LINK de /assets/ que falhou ou erro de chunk
1193      //                  (hardError), com ou sem app montado;
1194      //                - #root SEM filhos depois do DCL e da carência, com a
1195      //                  entrada que não rodou (sem __APP_BOOT), erro fatal ou
1196      //                  erro JS do próprio host. O React 19 desmonta a árvore
1197      //                  em erro não tratado, então #root vazio com erro é
1198      //                  queda; #root com filhos e erro é um módulo acessório
1199      //                  (eleições, NPS, anúncio) que falhou enquanto a rota
1200      //                  ainda baixa, e isso não é queda.
1201      //   'nuclear'  → recarga com limpeza dos caches do SW. Só com evidência:
1202      //                SW controlando a página E falha de ativo ou de chunk.
1203      //   'give_up'  → erro determinístico (sintaxe, ICU), mesma falha depois
1204      //                de uma recarga, ou tentativas esgotadas: overlay (a
1205      //                partir dos 12 s). Com o app montado, o aviso não
1206      //                bloqueante no lugar do overlay.
1207      //   'bot'      → a falha é real, mas é robô: só registra.
1208      function decideBlankAction(s) {
1209        if (!s.isBlank) return 'ok';
1210        if (s.chunkReloading) return 'wait';
1211        var settled = s.dcl && s.dclAgoMs >= DCL_GRACE_MS;
1212        var dead = !s.navigated && (s.hardError ||
1213          (!s.appMounted && settled && (s.fatal || s.ownError || !s.appBooted)));
1214        if (!dead) {
1215          if (s.elapsed >= SLOW_NOTICE_MS && !s.bot) return 'slow';
1216          return s.navigated ? 'spa_wait' : 'wait';
1217        }
1218        if (s.bot) return 'bot';
1219        if (s.fatal || s.attempts >= MAX_RETRIES) return 'give_up';
1220        if (s.repeat) return (s.nuclearEvidence && s.lastMethod !== 'nuclear') ? 'nuclear' : 'give_up';
1221        return (s.attempts >= 1 && s.nuclearEvidence) ? 'nuclear' : 'recover';
1222      }
1223
1224      // ── Tempo visível ──
1225      var visibleMs = 0;
1226      function isHidden() {
1227        try { return document.visibilityState === 'hidden' || document.hidden === true; } catch(e) { return false; }
1228      }
1229      var visibleSince = isHidden() ? null : Date.now();
1230      function visibleElapsed() {
1231        return visibleMs + (visibleSince === null ? 0 : Date.now() - visibleSince);
1232      }
1233      var pending = null;
1234      var timer = null;
1235      function arm() {
1236        if (timer) { clearTimeout(timer); timer = null; }
1237        if (!pending || isHidden()) return; // retoma no visibilitychange
1238        timer = setTimeout(function() {
1239          timer = null;
1240          if (!pending) return;
1241          if (isHidden() || visibleElapsed() < pending.target - 25) { arm(); return; }
1242          var p = pending; pending = null; p.fn();
1243        }, Math.max(0, pending.target - visibleElapsed()));
1244      }
1245      function at(target, fn) { pending = { target: target, fn: fn }; arm(); }
1246      document.addEventListener('visibilitychange', function() {
1247        if (isHidden()) {
1248          if (visibleSince !== null) { visibleMs += Date.now() - visibleSince; visibleSince = null; }
1249          if (timer) { clearTimeout(timer); timer = null; }
1250        } else {
1251          if (visibleSince === null) visibleSince = Date.now();
1252          arm();
1253        }
1254      });
1255
1256      var loggedSpa = false;
1257      var loggedSlow = false;
1258      var loggedGiveUp = false;
1259
1260      function shellVisible() {
1261        return !!document.getElementById('app-shell') && window.__APP_SHELL_REMOVED !== true;
1262      }
1263
1264      function runCheck(target) {
1265        if (recovering || finished) return;
1266        var phase = Math.round(target / 1000) + 's';
1267        window.__diag.phase = phase;
1268        var result = checkBlank();
1269        var root = document.getElementById('root');
1270        var sig = currentSig();
1271        var last = readLastReload();
1272        var chunkTs = chunkReloadTs();
1273        var hardError = hasHardError();
1274        var appMounted = !!(root && root.children.length > 0);
1275        var repeat = !!(last && last.sig === sig) ||
1276          (hardError && chunkTs > 0 && chunkTs < START && START - chunkTs < SESSION_WINDOW_MS);
1277        var elapsed = visibleElapsed();
1278        var action = decideBlankAction({
1279          isBlank: result.isBlank,
1280          bot: IS_BOT,
1281          navigated: location.pathname !== BOOT_PATH,
1282          appMounted: appMounted,
1283          appBooted: appBootMs() !== null,
1284          dcl: dclAt !== null,
1285          dclAgoMs: dclAt === null ? 0 : Date.now() - dclAt,
1286          hardError: hardError,
1287          ownError: ownErrors.length > 0,
1288          fatal: fatalError !== null,
1289          repeat: repeat,
1290          lastMethod: last ? last.method : null,
1291          nuclearEvidence: hasSwController() && (hardError || hasMimeEvidence()),
1292          attempts: getReloadCount(),
1293          elapsed: elapsed,
1294          chunkReloading: chunkTs >= START
1295        });
1296        var next = function() {
1297          if (target + RECHECK_MS <= WATCH_MAX_MS) at(target + RECHECK_MS, function() { runCheck(target + RECHECK_MS); });
1298        };
1299
1300        // Conteúdo na tela. Até os 12 s segue olhando, como o V3 (checagens de
1301        // 5 s e 12 s): é o que pega a navegação SPA logo depois da carga.
1302        if (action === 'ok') { clearReloadCount(); hideRecoveryUi(); if (target < OVERLAY_MIN_MS) next(); return; }
1303        if (action === 'wait') { next(); return; }
1304        if (action === 'spa_wait') {
1305          // Registra só o primeiro adiamento: é o dado que mede quantas
1306          // recargas falsas a guarda evitou. Robô só lento não gera linha.
1307          if (!loggedSpa && !IS_BOT) { loggedSpa = true; logIncident('blank_screen', phase, result.metrics, true, 'spa_recheck'); }
1308          next();
1309          return;
1310        }
1311        if (action === 'slow') {
1312          if (!loggedSlow) { loggedSlow = true; logIncident('blank_screen', phase, result.metrics, true, 'slow_boot'); }
1313          if (!shellVisible() || elapsed >= SLOW_NOTICE_SHELL_MS) showSlowNotice();
1314          next();
1315          return;
1316        }
1317        if (action === 'bot') {
1318          // Robô com a página quebrada de verdade: uma linha, sem recarga,
1319          // aviso ou overlay. Robô só lento não gera linha nenhuma.
1320          finished = true;
1321          logIncident('blank_screen', phase, result.metrics, false, 'bot_no_action', { sig: sig });
1322          return;
1323        }
1324        if (action === 'recover' || action === 'nuclear') {
1325          recoverAndReload(result.metrics, phase, action === 'nuclear' ? 'nuclear' : 'plain', sig);
1326          return;
1327        }
1328        // give_up: o overlay espera os 12 s; até lá, o leitor ainda pode ver
1329        // a rota pintar.
1330        if (elapsed < OVERLAY_MIN_MS) { next(); return; }
1331        if (!loggedGiveUp) { loggedGiveUp = true; logIncident('blank_screen', phase, result.metrics, false, null, { sig: sig }); }
1332        // App montado (só chega aqui com falha de ativo ou de chunk que se
1333        // repete): o aviso não bloqueante, e segue olhando. Um overlay por
1334        // cima de um app que ainda pode pintar a rota é pior que o branco.
1335        if (appMounted) { showSlowNotice(); next(); return; }
1336        finished = true;
1337        hideSlowNotice();
1338        if (typeof window.__removeAppShell === 'function') { try { window.__removeAppShell(); } catch(e) {} }
1339        showRecoveryOverlay(result);
1340        watchPaint();
1341      }
1342
1343      at(FIRST_CHECK_MS, function() { runCheck(FIRST_CHECK_MS); });
1344
1345      // Nó com estilo inline e texto via textContent (nada de HTML montado a
1346      // partir de mensagem de erro, UA ou URL).
1347      function node(tag, css, text) {
1348        var el = document.createElement(tag);
1349        if (css) el.style.cssText = css;
1350        if (text !== undefined) el.textContent = text;
1351        return el;
1352      }
1353
1354      // Aviso de boot lento: discreto, não bloqueia a página e não recarrega
1355      // sozinho. Some quando a rota pinta.
1356      function showSlowNotice() {
1357        if (IS_BOT || document.getElementById('pf-slow-notice')) return;
1358        var d = node('div', 'position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:99998;box-sizing:border-box;width:max-content;max-width:calc(100% - 32px);display:flex;align-items:center;gap:12px;padding:10px 10px 10px 16px;background:#0f172a;color:#f8fafc;border-radius:12px;box-shadow:0 8px 24px rgba(15,23,42,.25);font-family:Inter,-apple-system,system-ui,sans-serif;font-size:14px;font-weight:500;line-height:1.4');
1359        d.id = 'pf-slow-notice';
1360        d.setAttribute('role', 'status');
1361        d.setAttribute('aria-live', 'polite');
1362        d.appendChild(node('span', '', 'A página está demorando mais que o normal.'));
1363        var b = node('button', 'flex:none;padding:8px 14px;background:#fff;color:#0f172a;border:none;border-radius:8px;font-family:inherit;font-size:14px;font-weight:600;cursor:pointer', 'Recarregar');
1364        b.type = 'button';
1365        b.onclick = function() { clearReloadCount(); location.reload(); };
1366        d.appendChild(b);
1367        (document.body || document.documentElement).appendChild(d);
1368        watchPaint();
1369      }
1370      function hideSlowNotice() {
1371        var d = document.getElementById('pf-slow-notice');
1372        if (d && d.parentNode) d.parentNode.removeChild(d);
1373      }
1374      function hideRecoveryUi() {
1375        hideSlowNotice();
1376        var o = document.getElementById('ios-diag-overlay');
1377        if (o && o.parentNode) o.parentNode.removeChild(o);
1378      }
1379      // Com aviso ou overlay na tela, olha a cada meio segundo e tira os dois
1380      // assim que a rota pinta. A checagem de 3,5 s deixava o aviso por cima
1381      // da página pronta, e depois dos 180 s (ou do overlay) ninguém olhava.
1382      var paintTimer = null;
1383      function watchPaint() {
1384        if (paintTimer) return;
1385        paintTimer = setInterval(function() {
1386          if (checkBlank().isBlank) return;
1387          clearInterval(paintTimer);
1388          paintTimer = null;
1389          hideRecoveryUi();
1390        }, 500);
1391      }
1392
1393      function showRecoveryOverlay(result) {
1394        if (document.getElementById('ios-diag-overlay')) return; // Already showing
1395        var tried = getReloadCount() > 0;
1396        var d = node('div', 'position:fixed;inset:0;z-index:99999;background:#fafafa;padding:0;font-family:Inter,-apple-system,system-ui,sans-serif;overflow:auto;display:flex;flex-direction:column;align-items:center;justify-content:center');
1397        d.id = 'ios-diag-overlay';
1398        var box = node('div', 'max-width:420px;width:100%;padding:32px 24px;text-align:center');
1399        var icon = node('div', 'width:56px;height:56px;border-radius:16px;background:#fee2e2;display:flex;align-items:center;justify-content:center;margin:0 auto 20px');
1400        try {
1401          var NS = 'http://www.w3.org/2000/svg';
1402          var svg = document.createElementNS(NS, 'svg');
1403          var attrs = { width: '28', height: '28', viewBox: '0 0 24 24', fill: 'none', stroke: '#dc2626', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true' };
1404          for (var a in attrs) svg.setAttribute(a, attrs[a]);
1405          var shapes = [['circle', { cx: '12', cy: '12', r: '10' }], ['line', { x1: '12', y1: '8', x2: '12', y2: '12' }], ['line', { x1: '12', y1: '16', x2: '12.01', y2: '16' }]];
1406          for (var i = 0; i < shapes.length; i++) {
1407            var sh = document.createElementNS(NS, shapes[i][0]);
1408            for (var sa in shapes[i][1]) sh.setAttribute(sa, shapes[i][1][sa]);
1409            svg.appendChild(sh);
1410          }
1411          icon.appendChild(svg);
1412        } catch(e) {}
1413        box.appendChild(icon);
1414        box.appendChild(node('h2', 'font-size:20px;font-weight:700;color:#0f172a;margin:0 0 8px;letter-spacing:-0.02em', 'Algo deu errado'));
1415        box.appendChild(node('p', 'font-size:14px;color:#64748b;margin:0 0 24px;line-height:1.5', tried
1416          ? 'A página não carregou corretamente. Tentamos recarregar automaticamente, mas o problema persiste.'
1417          : 'A página não carregou corretamente. Recarregue ou volte para a página inicial.'));
1418        var reload = node('button', 'width:100%;padding:14px;background:#0f172a;color:#fff;border:none;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;letter-spacing:-0.01em', 'Recarregar Página');
1419        reload.type = 'button';
1420        reload.onclick = function() { window.__pfNuclearReload(); };
1421        reload.onmouseover = function() { reload.style.background = '#1e293b'; };
1422        reload.onmouseout = function() { reload.style.background = '#0f172a'; };
1423        box.appendChild(reload);
1424        var home = node('button', 'width:100%;padding:12px;background:transparent;color:#64748b;border:1px solid #e2e8f0;border-radius:12px;font-size:14px;font-weight:500;cursor:pointer;margin-top:10px', 'Ir para a página inicial');
1425        home.type = 'button';
1426        home.onclick = function() { location.href = '/'; };
1427        home.onmouseover = function() { home.style.borderColor = '#94a3b8'; home.style.color = '#334155'; };
1428        home.onmouseout = function() { home.style.borderColor = '#e2e8f0'; home.style.color = '#64748b'; };
1429        box.appendChild(home);
1430        // Collapsible technical details (dev-friendly, user-hidden)
1431        var det = node('details', 'margin-top:24px;text-align:left;width:100%');
1432        det.appendChild(node('summary', 'font-size:12px;color:#94a3b8;cursor:pointer;font-weight:500;user-select:none', 'Informações técnicas'));
1433        det.appendChild(node('pre', 'background:#0f172a;color:#4ade80;padding:14px;border-radius:10px;font-size:10px;overflow:auto;white-space:pre-wrap;word-break:break-all;max-height:200px;margin-top:8px;line-height:1.5',
1434          result.info
1435          + '\n\nErros (' + errors.length + '):\n' + (errors.length ? errors.join('\n') : '(nenhum)')
1436          + '\n\nUA: ' + navigator.userAgent
1437          + '\n\nURL: ' + location.origin + safePath(location.pathname + location.search)
1438          + '\n\nTentativas: ' + getReloadCount()));
1439        box.appendChild(det);
1440        d.appendChild(box);
1441        document.body.appendChild(d);
1442      }
1443    })();
1444  </script>
1444
1445  
1445<script>
1446    // ── Edge skeleton (#app-shell) removal ──
1447    // The edge (seo-meta-injection.ts) injects a full above-the-fold skeleton as
1448    // a SIBLING overlay of #root for portal homes. Remove it the instant React
1449    // paints REAL page content into #root: a <main>, <article> or <h1>, or the
1450    // explicit window.__removeAppShell() (MainLayout.tsx e edgeArticleShell.ts).
1451    // NOT on #root.firstElementChild: App renders an empty <div class="min-h-
1452    // screen bg-white"> wrapper on React's very first commit, long before the
1453    // lazy PortalApp->PortalHome chunks paint. And NOT on text length: o
1454    // GlobalAudioPlayer e o PortalCookieBanner, montados fora da rota, injetam
1455    // <style> no #root (749 caracteres de @keyframes), e o Toaster do sonner
1456    // monta uma <section>. Com "texto > 30", o esqueleto saía aos ~1 s e o
1457    // leitor via 2 a 3 s de branco até a rota pintar (medido em 29/09/2026,
1458    // 5 de 5 cargas da home do JB; até 10 s no celular lento).
1459    (function () {
1460      var root = document.getElementById('root');
1461      var shell = document.getElementById('app-shell');
1462      var removed = false;
1463      var mo = null;
1464      var iv = null;
1465      // Shell de MATÉRIA: guarda a referência para a página de matéria mostrar
1466      // esse conteúdo se a consulta dela falhar depois de o shell ter saído
1467      // (src/features/news/edgeArticleShell.ts). Só guarda; a remoção é a mesma.
1468      if (shell && shell.getAttribute('data-shell') === 'article') {
1469        window.__EDGE_ARTICLE_SHELL = { node: shell, path: location.pathname };
1470      }
1471      function hasRealContent() {
1472        return !!root && !!root.querySelector('main, article, h1');
1473      }
1474      function removeShell() {
1475        if (removed) return;
1476        removed = true;
1477        window.__APP_SHELL_REMOVED = true; // watchdog readiness flag
1478        if (mo) mo.disconnect();
1479        if (iv) clearInterval(iv);
1480        var s = document.getElementById('app-shell');
1481        if (!s) return;
1482        s.style.transition = 'opacity .25s ease';
1483        s.style.opacity = '0';
1484        setTimeout(function () { if (s.parentNode) s.parentNode.removeChild(s); }, 280);
1485      }
1486      // Secondary trigger: MainLayout's mount effect calls this after real content.
1487      window.__removeAppShell = removeShell;
1488      if (shell && root) {
1489        if (hasRealContent()) {
1490          removeShell();
1491        } else if ('MutationObserver' in window) {
1492          mo = new MutationObserver(function () {
1493            if (hasRealContent()) removeShell();
1494          });
1495          mo.observe(root, { childList: true, subtree: true });
1496        } else {
1497          iv = setInterval(function () {
1498            if (hasRealContent()) removeShell();
1499          }, 80);
1500        }
1501        // Rede de segurança, aos 25 s (era 8 s incondicional, e aos 8 s uma
1502        // página só lenta perdia o esqueleto e mostrava branco). Cobre a rota
1503        // que pinta sem <main>/<article>/<h1>: com qualquer conteúdo visível no
1504        // #root, o esqueleto sai. Com o #root ainda em branco, fica, e o
1505        // detector de tela branca decide: boot morto recarrega ou vai para o
1506        // overlay (que tira o esqueleto); boot só lento mostra o aviso com
1507        // "Recarregar" por cima do esqueleto. Sem o detector, sai aos 25 s.
1508        var safety = function () {
1509          if (removed) return;
1510          var isBlank = window.__pfRootIsBlank;
1511          if (typeof isBlank === 'function') {
1512            var blank = true;
1513            try { blank = isBlank() === true; } catch (e) { blank = false; }
1514            if (blank) { setTimeout(safety, 4000); return; }
1515          }
1516          removeShell();
1517        };
1518        setTimeout(safety, 25000);
1519      } else {
1520        // No skeleton on this page (dev / non-portal / logged-in): mark ready,
1521        // como antes (o detector V4 não depende mais desta flag).
1522        window.__APP_SHELL_REMOVED = true;
1523      }
1524    })();
1525  </script>
1525
1526  
1526<script>
1527    (function() {
1528      var h = location.hostname;
1529      var p = location.pathname;
1530      var isAdminPage = h.startsWith('admin.') || h.startsWith('console.') || p.startsWith('/admin') || p.startsWith('/console');
1531      if (isAdminPage) {
1532        var preloaderContainer = document.getElementById('global-admin-preloader-container');
1533        if (preloaderContainer) {
1534          preloaderContainer.innerHTML = '<style>.uni-loader{position:fixed;top:0;left:0;width:100%;height:100%;z-index:99999;background:#fafafa;transition:opacity .4s ease}.uni-loader-bar{position:absolute;top:0;left:0;width:100%;height:2px;overflow:hidden}.uni-loader-bar::after{content:"";position:absolute;top:0;left:-40%;width:40%;height:100%;background:linear-gradient(90deg,transparent,rgba(0,0,0,.12),transparent);animation:uni-shimmer 1.4s ease-in-out infinite}@keyframes uni-shimmer{0%{left:-40%}100%{left:100%}}@media(prefers-color-scheme:dark){.uni-loader{background:#0a0a0a}.uni-loader-bar::after{background:linear-gradient(90deg,transparent,rgba(255,255,255,.08),transparent)}}</style><div id="global-admin-preloader" class="uni-loader"><div class="uni-loader-bar"></div></div>';
1535        }
1536      }
1537    })();
1538  </script>
1538
1539  <!-- WebMCP: Expose site tools to AI agents via browser -->
1540  
1540<script>
1541    (function() {
1542      // WebMCP API — navigator.modelContext.provideContext()
1543      // Spec: https://webmachinelearning.github.io/webmcp/
1544      // No-op if browser doesn't support it (progressive enhancement)
1545      if (typeof navigator !== 'undefined' && 'modelContext' in navigator && navigator.modelContext && typeof navigator.modelContext.provideContext === 'function') {
1546        try {
1547          navigator.modelContext.provideContext({
1548            tools: [
1549              {
1550                name: 'search_articles',
1551                description: 'Search published news articles on this portal. Returns headlines, summaries, categories, and direct URLs.',
1552                inputSchema: {
1553                  type: 'object',
1554                  properties: {
1555                    query: { type: 'string', description: 'Search query text' },
1556                    category: { type: 'string', description: 'Category filter (politica, educacao, saude, seguranca, cultura, esportes, economia, transporte, meio-ambiente)' },
1557                    limit: { type: 'number', description: 'Maximum results to return (default: 10)' }
1558                  },
1559                  required: ['query']
1560                }
1561              },
1562              {
1563                name: 'get_portal_info',
1564                description: 'Get structured information about the current portal including editorial policies, coverage areas, contact details, and citation format.',
1565                inputSchema: {
1566                  type: 'object',
1567                  properties: {
1568                    format: { type: 'string', enum: ['full', 'summary'], description: 'Response detail level' }
1569                  }
1570                }
1571              },
1572              {
1573                name: 'get_latest_news',
1574                description: 'Get the most recent published news articles from this portal with headlines, summaries, and publication timestamps.',
1575                inputSchema: {
1576                  type: 'object',
1577                  properties: {
1578                    count: { type: 'number', description: 'Number of articles to return (default: 5, max: 20)' },
1579                    category: { type: 'string', description: 'Optional category filter' }
1580                  }
1581                }
1582              },
1583              {
1584                name: 'get_weather',
1585                description: 'Get current weather conditions and forecast for the local region.',
1586                inputSchema: {
1587                  type: 'object',
1588                  properties: {
1589                    days: { type: 'number', description: 'Forecast days (1-7, default: 1)' }
1590                  }
1591                }
1592              }
1593            ]
1594          });
1595        } catch(e) {
1596          // Silently fail — WebMCP not supported
1597        }
1598      }
1599    })();
1600  </script>
1600
1601</body>
1602
1603</html>

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.