PageSourceSearch

https://bandfmcampinas.com.br/assets/js/uhost.js?v=57

js bandfmcampinas.com.br collected 2026-09-24 14:34:19 UTC 45,131 bytes, 913 lines download raw bytes

1/* UHOST 2027 — interações. Vanilla JS, sem dependências.
2   Movimento sutil e elegante (regra do design system): fade/slide, contadores,
3   texto digitando, accordion. Respeita prefers-reduced-motion. */
4(function () {
5  'use strict';
6  var reduz = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
7
8  /* ---------- tema claro/escuro ---------- */
9  document.querySelectorAll('[data-tema-btn]').forEach(function (btn) {
10    btn.addEventListener('click', function () {
11      var root = document.documentElement;
12      var claro = root.getAttribute('data-tema') === 'claro';
13      if (claro) { root.removeAttribute('data-tema'); } else { root.setAttribute('data-tema', 'claro'); }
14      try { localStorage.setItem('uh-tema', claro ? 'escuro' : 'claro'); } catch (e) {}
15    });
16  });
17
18  /* ---------- header: sombra/blur ao rolar ---------- */
19  var hd = document.querySelector('[data-header]');
20  if (hd) {
21    var onScroll = function () { hd.style.boxShadow = window.scrollY > 8 ? 'var(--shadow-card)' : 'none'; };
22    onScroll();
23    window.addEventListener('scroll', onScroll, { passive: true });
24  }
25
26  /* ---------- menu mobile ---------- */
27  var burger = document.querySelector('[data-burger]');
28  if (burger) {
29    burger.addEventListener('click', function () {
30      var aberto = document.body.classList.toggle('is-menu');
31      burger.setAttribute('aria-expanded', aberto ? 'true' : 'false');
32      document.body.style.overflow = aberto ? 'hidden' : '';
33    });
34    document.querySelectorAll('[data-mnav] a').forEach(function (a) {
35      a.addEventListener('click', function () {
36        document.body.classList.remove('is-menu');
37        burger.setAttribute('aria-expanded', 'false');
38        document.body.style.overflow = '';
39      });
40    });
41  }
42
43  /* ---------- reveal on scroll ---------- */
44  var revs = document.querySelectorAll('.rv');
45  if (reduz || !('IntersectionObserver' in window)) {
46    revs.forEach(function (el) { el.classList.add('on'); });
47  } else {
48    var stagger = new WeakMap();
49    var io = new IntersectionObserver(function (entries) {
50      entries.forEach(function (en) {
51        if (en.isIntersecting) {
52          en.target.classList.add('on');
53          io.unobserve(en.target);
54        }
55      });
56    }, { threshold: 0.12, rootMargin: '0px 0px -8% 0px' });
57    /* dá um pequeno atraso escalonado a grupos de irmãos (cards em grade) */
58    revs.forEach(function (el) {
59      var p = el.parentElement, i = 0, n = el;
60      while ((n = n.previousElementSibling)) { if (n.classList && n.classList.contains('rv')) i++; }
61      if (i > 0 && i < 8) el.style.transitionDelay = (i * 0.07).toFixed(2) + 's';
62      io.observe(el);
63    });
64  }
65
66  /* ---------- contadores animados ----------
67     Padrão: 1,6s. Com data-dur, vira ODÔMETRO EM RAJADAS: sobe de perto do
68     alvo até ele em poucas rajadas e, dali em diante, NÃO para de vez: segue
69     subindo um passo visível com pausas cada vez maiores (número vivo).
70     Só começa quando o usuário VÊ os números (IntersectionObserver).
71
72     🚨 O disparo parte de 90% do alvo, NÃO de zero (medido em 2026-09-10:
73     partindo do 7% a faixa levava ~40s para chegar em 14,7 mil e o visitante
74     lia "1,0 mil rádios atendidas" — o número que ESTÁ na tela é a única prova
75     social do site, porque a marca proíbe depoimento. Contador que subestima a
76     empresa em 14x é pior do que contador nenhum). O movimento continua; o que
77     mudou é de ONDE ele parte e em quanto tempo alcança (~3s). */
78  function animaContador(el) {
79    var bruto = el.getAttribute('data-count') || el.textContent;
80    var sufixo = el.getAttribute('data-suffix') || '';
81    var numerico = /^[\d.,]+$/.test(bruto);
82    if (!numerico || reduz) { el.textContent = bruto + sufixo; return; }
83    var alvo = parseFloat(bruto.replace(/\./g, '').replace(',', '.'));
84    var decimais = bruto.indexOf(',') >= 0 ? 1 : 0;
85    var fmt = function (n) { return n.toLocaleString('pt-BR', { minimumFractionDigits: decimais, maximumFractionDigits: decimais }); };
86    var ultimo = -1;
87    function pinta(v) {
88      var m = decimais ? Math.round(v * 10) : Math.round(v);
89      if (m !== ultimo) { el.textContent = fmt(v) + sufixo; ultimo = m; }
90    }
91    var easeOut = function (p) { return 1 - Math.pow(1 - p, 3); };
92    var easeIO  = function (p) { return p < .5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; };
93    var atual = 0;
94    function anima(de, ate, dur, easing, fim) {
95      var t0 = null;
96      function tick(t) {
97        if (t0 === null) t0 = t;
98        var p = Math.min((t - t0) / dur, 1);
99        pinta(de + (ate - de) * easing(p));
100        if (p < 1) { requestAnimationFrame(tick); } else { atual = ate; if (fim) fim(); }
101      }
102      requestAnimationFrame(tick);
103    }
104
105    if (!el.getAttribute('data-dur')) {
106      /* Contador comum: parte dos mesmos 90% (é o valor que já está pintado na
107         largada). Partindo de zero, o "99,9% de uptime" caía para 0,0% na frente
108         do visitante antes de subir — pior do que não animar. */
109      atual = alvo * 0.9;
110      anima(atual, alvo, 1200, easeOut, null);
111      return;
112    }
113
114    /* modo rajadas */
115    var passo = decimais ? 0.1 : 1;                         /* menor incremento visível */
116    var teto  = alvo * 1.1;                                 /* daqui em diante vira conta-gotas */
117    function deriva(pausa) {
118      /* nunca cessa: sobe um passo visível com pausas que crescem só até 18s;
119         passado o teto (+10% do alvo), o passo continua mas a cada 5min */
120      setTimeout(function () {
121        anima(atual, atual + passo, 700, easeIO, function () {
122          deriva(atual < teto ? Math.min(pausa * 1.2, 18000) : 300000);
123        });
124      }, pausa * (0.85 + Math.random() * 0.3));
125    }
126    function proximaRajada() {
127      var resta = alvo - atual;
128      if (resta <= 0) { pinta(alvo); deriva(2200); return; }
129      var pausa = 320 + Math.random() * 480;                /* respiro parado */
130      setTimeout(function () {
131        var pedaco = resta * (0.34 + Math.random() * 0.30); /* sobe um pedaço */
132        if (resta - pedaco < alvo * 0.02) pedaco = resta;   /* reta final: alcança o número base */
133        anima(atual, atual + pedaco, 420 + Math.random() * 380, easeIO, proximaRajada);
134      }, pausa);
135    }
136    var largada = alvo * 0.9;             /* nunca começa em zero: ver o comentário acima */
137    atual = largada; pinta(largada);
138    anima(largada, alvo * 0.97, 1200, easeOut, proximaRajada);
139  }
140  var counters = document.querySelectorAll('[data-count]');
141  if (counters.length) {
142    if (!('IntersectionObserver' in window)) {
143      counters.forEach(animaContador);
144    } else {
145      var ioc = new IntersectionObserver(function (entries) {
146        entries.forEach(function (en) {
147          if (en.isIntersecting) { animaContador(en.target); ioc.unobserve(en.target); }
148        });
149      }, { threshold: 0.5 });
150      counters.forEach(function (el) {
151        /* Antes de disparar mostra 90% do alvo, não zero: a faixa fica logo abaixo
152           da dobra e no celular muita gente passa por ela antes do IntersectionObserver
153           acordar — quem via "0,0% de uptime" lia isso como o nosso uptime. */
154        if (!reduz && /^[\d.,]+$/.test(el.getAttribute('data-count') || '')) {
155          var suf = el.getAttribute('data-suffix') || '';
156          var cru = el.getAttribute('data-count') || '';
157          var dec = cru.indexOf(',') >= 0 ? 1 : 0;
158          var ini = parseFloat(cru.replace(/\./g, '').replace(',', '.')) * 0.9;
159          el.textContent = ini.toLocaleString('pt-BR', { minimumFractionDigits: dec, maximumFractionDigits: dec }) + suf;
160        }
161        ioc.observe(el);
162      });
163    }
164  }
165
166  /* ---------- accordion (FAQ) ---------- */
167  document.querySelectorAll('[data-faq]').forEach(function (faq) {
168    faq.querySelectorAll('.uh-q').forEach(function (q) {
169      var btn = q.querySelector('.uh-q__b');
170      var painel = q.querySelector('.uh-q__a');
171      /* abre o que já vem com is-open */
172      if (q.classList.contains('is-open')) painel.style.maxHeight = painel.scrollHeight + 'px';
173      btn.addEventListener('click', function () {
174        var abrindo = !q.classList.contains('is-open');
175        faq.querySelectorAll('.uh-q').forEach(function (o) {
176          o.classList.remove('is-open');
177          o.querySelector('.uh-q__b').setAttribute('aria-expanded', 'false');
178          o.querySelector('.uh-q__a').style.maxHeight = null;
179        });
180        if (abrindo) {
181          q.classList.add('is-open');
182          btn.setAttribute('aria-expanded', 'true');
183          painel.style.maxHeight = painel.scrollHeight + 'px';
184        }
185      });
186    });
187  });
188
189  /* ---------- celular do app: troca de rádio ---------- */
190  document.querySelectorAll('.uh-cels').forEach(function (grupo) {
191    var bts = [].slice.call(grupo.querySelectorAll('[data-cel-b]'));
192    var fs  = [].slice.call(grupo.querySelectorAll('[data-cel]'));
193    if (!bts.length || bts.length !== fs.length) return;
194    function mostra(i, focar) {
195      bts.forEach(function (b, j) {
196        b.classList.toggle('is-on', j === i);
197        b.setAttribute('aria-selected', j === i ? 'true' : 'false');
198        b.tabIndex = j === i ? 0 : -1;
199        fs[j].classList.toggle('is-on', j === i);
200      });
201      if (focar) bts[i].focus();
202    }
203    bts.forEach(function (b, i) {
204      b.addEventListener('click', function () { mostra(i, false); });
205      b.addEventListener('keydown', function (e) {
206        var n = bts.length, alvo = -1;
207        if (e.key === 'ArrowRight' || e.key === 'ArrowDown') alvo = (i + 1) % n;
208        else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') alvo = (i - 1 + n) % n;
209        if (alvo < 0) return;
210        e.preventDefault(); mostra(alvo, true);
211      });
212    });
213  });
214
215  /* ---------- [data-troca]: botão -> painel (uh-perfis, uh-kbps) ----------
216     Um só bloco serve aos dois componentes de 10/09. Padrão de tablist da
217     WAI-ARIA: seta troca o painel, Home/End vão às pontas, e o foco segue a
218     seleção (tabindex roving). Sem JS a seção continua legível — o primeiro
219     par já sai marcado do PHP e os outros painéis ficam no HTML para o Google. */
220  document.querySelectorAll('[data-troca]').forEach(function (grupo) {
221    var bts = [].slice.call(grupo.querySelectorAll('[data-troca-b]'));
222    var pns = [].slice.call(grupo.querySelectorAll('.uh-troca2__p'));
223    if (!bts.length || bts.length !== pns.length) return;
224
225    function mostra(i, focar) {
226      bts.forEach(function (b, j) {
227        var on = j === i;
228        b.classList.toggle('is-on', on);
229        b.setAttribute('aria-selected', on ? 'true' : 'false');
230        b.tabIndex = on ? 0 : -1;
231        pns[j].hidden = !on;
232        pns[j].classList.toggle('is-on', on);
233      });
234      if (focar) bts[i].focus();
235    }
236    bts.forEach(function (b, i) {
237      b.addEventListener('click', function () { mostra(i, false); });
238      b.addEventListener('keydown', function (e) {
239        var k = e.key, n = bts.length, alvo = -1;
240        if (k === 'ArrowDown' || k === 'ArrowRight') alvo = (i + 1) % n;
241        else if (k === 'ArrowUp' || k === 'ArrowLeft') alvo = (i - 1 + n) % n;
242        else if (k === 'Home') alvo = 0;
243        else if (k === 'End') alvo = n - 1;
244        if (alvo < 0) return;
245        e.preventDefault(); mostra(alvo, true);
246      });
247    });
248  });
249
250  /* ---------- cards de plano: lista recolhida no celular ----------
251     O HTML sai com <details open> (sem JS a lista fica inteira, e é assim que o
252     Google a lê). Aqui, e só abaixo de 720px, os três fecham: empilhados, os
253     cards somavam 3.337px — quatro telas de iPhone antes de o visitante ver as
254     três opções. Quem abriu um mantém aberto na volta do resize. */
255  var incs = document.querySelectorAll('.uh-plan__inc');
256  if (incs.length) {
257    var estreito = window.matchMedia('(max-width:720px)');
258    var aplicaInc = function () {
259      incs.forEach(function (d) {
260        if (d.dataset.tocado === '1') return;
261        d.open = !estreito.matches;
262      });
263    };
264    incs.forEach(function (d) {
265      d.addEventListener('toggle', function () {
266        if (estreito.matches) d.dataset.tocado = '1';
267      });
268    });
269    aplicaInc();
270    if (estreito.addEventListener) { estreito.addEventListener('change', aplicaInc); }
271    else if (estreito.addListener) { estreito.addListener(aplicaInc); }
272  }
273
274  /* ---------- hero: texto digitando ----------
275     🚨 O <h1> depende desta palavra para fazer sentido ("...emissora grande: site").
276     Enquanto ela apaga, o título fica truncado nos dois-pontos — então o ciclo
277     dá UMA volta pelas palavras, volta para a primeira e PARA de vez (medido em
278     2026-09-10: em loop infinito o título passava ~48% do tempo incompleto, e
279     era assim que ele aparecia em print e para quem voltava à aba). */
280  var type = document.querySelector('[data-type]');
281  if (type) {
282    var palavras;
283    try { palavras = JSON.parse(type.getAttribute('data-type')); } catch (e) { palavras = []; }
284    if (reduz || !palavras.length) {
285      type.textContent = palavras[0] || '';
286    } else {
287      var wi = 0, txt = '', apagando = false, voltas = 0;
288      (function loop() {
289        var palavra = palavras[wi];
290        if (!apagando) {
291          txt = palavra.slice(0, txt.length + 1);
292          type.textContent = txt;
293          if (txt === palavra) {
294            /* deu a volta e voltou à primeira palavra: assenta e não mexe mais */
295            if (voltas >= 1 && wi === 0) return;
296            apagando = true; setTimeout(loop, 2200); return;
297          }
298        } else {
299          txt = palavra.slice(0, txt.length - 1);
300          type.textContent = txt;
301          if (txt === '') {
302            apagando = false;
303            wi = (wi + 1) % palavras.length;
304            if (wi === 0) voltas++;
305          }
306        }
307        setTimeout(loop, apagando ? 45 : 95);
308      })();
309    }
310  }
311
312  /* ---------- lightbox: ampliar prints ao clicar ---------- */
313  var lb = document.querySelector('[data-lb]');
314  if (lb) {
315    var lbImg = lb.querySelector('.uh-lb__img');
316    var lbCap = lb.querySelector('.uh-lb__cap');
317    var alvos = '.uh-shot, .uh-show__media, .uh-strip, .uh-phone, .uh-wide, .uh-frame';
318    function abrirLb(src, cap) {
319      lbImg.src = src; lbImg.alt = cap || '';
320      lbCap.textContent = cap || '';
321      lb.classList.add('is-open'); lb.setAttribute('aria-hidden', 'false');
322      document.body.style.overflow = 'hidden';
323    }
324    function fecharLb() {
325      lb.classList.remove('is-open'); lb.setAttribute('aria-hidden', 'true');
326      document.body.style.overflow = '';
327      setTimeout(function () { if (!lb.classList.contains('is-open')) lbImg.src = ''; }, 320);
328    }
329    document.addEventListener('click', function (ev) {
330      var el = ev.target.closest(alvos);
331      if (!el || lb.contains(el)) return;
332      if (el.closest('a')) return; /* imagem dentro de link navega, não amplia */
333      // fonte em alta: data-zoom (showcase/strip) ou o <img> interno (moldura/phone)
334      var src = el.getAttribute('data-zoom');
335      var cap = el.getAttribute('data-cap');
336      if (!src) { var im = el.querySelector('img'); if (im) { src = im.currentSrc || im.src; cap = cap || im.alt; } }
337      if (src) { ev.preventDefault(); abrirLb(src, cap); }
338    });
339    lb.addEventListener('click', function (ev) { if (ev.target === lb || ev.target.closest('[data-lb-close]')) fecharLb(); });
340    document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape' && lb.classList.contains('is-open')) fecharLb(); });
341  }
342
343  /* ---------- página Alexa: conversa "você diz → Alexa responde" ---------- */
344  var say = document.querySelector('[data-uhx-say]');
345  var play = document.querySelector('[data-uhx-play]');
346  if (say && play) {
347    var radios = ['a minha rádio', 'Nativa FM', 'Band FM Campinas', 'a sua rádio', 'Notícia FM'];
348    if (reduz) {
349      say.textContent = radios[0]; play.textContent = radios[0];
350    } else {
351      var ri = 0;
352      function digita(el, txt, vel, fim) {
353        var i = 0;
354        (function passo() {
355          el.textContent = txt.slice(0, i++);
356          if (i <= txt.length) { setTimeout(passo, vel); } else if (fim) { setTimeout(fim, 0); }
357        })();
358      }
359      (function cicloAlexa() {
360        var nome = radios[ri];
361        play.textContent = '';
362        digita(say, nome, 85, function () {
363          setTimeout(function () {
364            digita(play, nome, 55, function () {
365              setTimeout(function () {
366                say.textContent = ''; play.textContent = '';
367                ri = (ri + 1) % radios.length;
368                setTimeout(cicloAlexa, 500);
369              }, 2600);
370            });
371          }, 900);
372        });
373      })();
374    }
375  }
376
377  /* ---------- leque de celulares: o scroll abre o leque ---------- */
378  /* o leque só é dirigido pelo scroll em tela larga; no celular o CSS o deixa
379     aberto e parado (ver o comentário em .uh-fan no uhost.css). */
380  var fans = document.querySelectorAll('[data-fan]');
381  if (fans.length && !reduz && window.matchMedia('(min-width:721px)').matches) {
382    var fanTick = false;
383    function atualizaFan() {
384      fanTick = false;
385      var vh = window.innerHeight;
386      fans.forEach(function (f) {
387        var r = f.getBoundingClientRect();
388        /* 0 quando o leque entra embaixo; 1 quando chega ao meio da tela */
389        var p = (vh - r.top - vh * 0.18) / (vh * 0.55);
390        p = Math.max(0, Math.min(1, p));
391        f.style.setProperty('--fanp', p.toFixed(3));
392      });
393    }
394    window.addEventListener('scroll', function () {
395      if (!fanTick) { fanTick = true; requestAnimationFrame(atualizaFan); }
396    }, { passive: true });
397    atualizaFan();
398  }
399
400  /* ---------- esferas do hero: parallax sutil no mouse ---------- */
401  if (!reduz && window.matchMedia('(pointer:fine)').matches) {
402    var orbs = document.querySelectorAll('.uh-orb');
403    if (orbs.length) {
404      window.addEventListener('mousemove', function (ev) {
405        var x = (ev.clientX / window.innerWidth - 0.5);
406        var y = (ev.clientY / window.innerHeight - 0.5);
407        orbs.forEach(function (o, i) {
408          var f = (i + 1) * 14;
409          o.style.transform = 'translate(' + (x * f).toFixed(1) + 'px,' + (y * f).toFixed(1) + 'px)';
410        });
411      }, { passive: true });
412    }
413  }
414})();
415
416
417/* ===== live demo do hero (#uhld) ===== */
418(function(){
419  const root=document.querySelector('[data-demo]'); if(!root) return;
420  const prev=root.querySelector('[data-prev]'), scr=root.querySelector('[data-scroll]');
421  const cursor=root.querySelector('[data-cursor]'), sceneEl=root.querySelector('[data-scene]'), actEl=root.querySelector('[data-act]');
422  const progress=root.querySelector('[data-progress]');
423  const reduce=matchMedia('(prefers-reduced-motion:reduce)').matches;
424  const swBtns=[...root.querySelectorAll('[data-sw] button')];
425
426  function tab(name){root.querySelectorAll('.tabs button').forEach(b=>b.classList.toggle('on',b.dataset.tab===name));
427    root.querySelectorAll('.pane').forEach(p=>p.classList.toggle('on',p.dataset.pane===name));}
428  function hot(sel){root.querySelectorAll('.ctl').forEach(c=>c.classList.toggle('hot',c.dataset.c===sel));}
429  function moveTo(el,ox=.5,oy=.5){if(!el)return;const pr=prev.getBoundingClientRect(),r=el.getBoundingClientRect();
430    cursor.style.transform=`translate(${r.left-pr.left+r.width*ox}px,${r.top-pr.top+r.height*oy}px)`;}
431  function click(){cursor.classList.remove('click');void cursor.offsetWidth;cursor.classList.add('click');}
432  function scrollTo(px){scr.style.transform=`translateY(${-px}px)`;}
433  const ICO={cor:'#F',fonte:'#F',tema:'#F'};
434  function act(n,label){actEl.querySelector('i').textContent=n;actEl.querySelector('span').textContent=label;actEl.classList.add('show');}
435  function scene(strong,em){sceneEl.querySelector('b').textContent=strong;sceneEl.querySelector('em').textContent=em||'';sceneEl.classList.add('show');}
436
437  function color(hex,btn){prev.style.setProperty('--brand',hex);swBtns.forEach(b=>b.classList.toggle('sel',b===btn));}
438  function font(p){prev.style.setProperty('--fs',(0.82+p*0.85).toFixed(2));root.querySelector('[data-f]').style.width=(p*100)+'%';root.querySelector('[data-k]').style.left=(p*100)+'%';}
439  function tema(t,btn){prev.style.setProperty('--rad',t==='wave'?'2px':t==='neon'?'20px':'14px');
440    prev.style.setProperty('--heroBg',t==='neon'?'linear-gradient(160deg,#171a2e,#101117)':t==='wave'?'#0c0d10':'#101117');
441    root.querySelectorAll('[data-tema] button').forEach(b=>b.classList.toggle('sel',b===btn));}
442  function player(p,btn){prev.classList.toggle('player-b',p==='b');root.querySelectorAll('[data-plr] button').forEach(b=>b.classList.toggle('sel',b===btn));}
443  function mod(name,on){const m=root.querySelector('[data-mod="'+name+'"]');if(m)m.classList.toggle('show',on!==false);
444    const r=root.querySelector('[data-r="'+name+'"]');if(r)r.classList.add('on');}
445  function toggle(name){root.querySelector('[data-'+name+']').classList.add('show');const r=root.querySelector('[data-r="'+name+'"]');if(r)r.classList.add('on');}
446  const ORD={slider:10,top:30,news:40,prog:50,pod:60,banner:70};
447  function setOrder(){root.querySelectorAll('.mod').forEach(m=>{m.style.transform='';m.style.transition='';
448    m.style.order=m.classList.contains('mod--hero')?20:ORD[m.dataset.mod];});}
449  function flip(mut){const mods=[...root.querySelectorAll('.mod')];const b=new Map(mods.map(m=>[m,m.getBoundingClientRect().top]));mut();
450    mods.forEach(m=>{const dy=b.get(m)-m.getBoundingClientRect().top;if(Math.abs(dy)>1){m.style.transition='none';m.style.transform='translateY('+dy+'px)';
451      requestAnimationFrame(()=>{m.style.transition='transform .55s var(--ease)';m.style.transform='';});}});}
452  function lift(name,on){const m=root.querySelector('[data-mod="'+name+'"]');
452if(m)m.classList.toggle('lift',on!==false);}
453  function moveMod(name,ord){flip(()=>{const m=root.querySelector('[data-mod="'+name+'"]');if(m)m.style.order=ord;});}
454  // grips (aparecem no modo edição)
455  root.querySelectorAll('.mod[data-mod]').forEach(m=>{const g=document.createElement('span');g.className='grip';
456    g.innerHTML='<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="6" r="1.4"/><circle cx="15" cy="6" r="1.4"/><circle cx="9" cy="12" r="1.4"/><circle cx="15" cy="12" r="1.4"/><circle cx="9" cy="18" r="1.4"/><circle cx="15" cy="18" r="1.4"/></svg>';
457    m.appendChild(g);});
458
459  // ---------- timeline (segundos) ----------
460  const q=s=>root.querySelector(s);
461  const L=[
462    // ATO 1 — Aparência
463    [0.2, ()=>{act('1','A cara da sua rádio');tab('aparencia');scene('Este é o site da sua rádio','tudo por um painel');}],
464    [1.8, ()=>{hot('cor');moveTo(swBtns[1]);}],
465    [2.6, ()=>{click();color('#7C5CFF',swBtns[1]);scene('Cor da marca','muda no site na hora');}],
466    [3.4, ()=>{moveTo(swBtns[2]);}],[3.9,()=>{click();color('#22C55E',swBtns[2]);}],
467    [4.6, ()=>{moveTo(swBtns[0]);}],[5.1,()=>{click();color('#FAC643',swBtns[0]);}],
468    [5.8, ()=>{hot('fonte');moveTo(q('[data-k]'));scene('Tipografia','arraste e o texto cresce');}],
469    [6.6, ()=>{font(0.9);}],[7.5,()=>{font(0.2);}],[8.3,()=>{font(0.5);}],
470    [9.1, ()=>{hot('tema');moveTo(q('[data-tema] [data-t=neon]'));scene('Troque o tema','4 estilos, um clique');}],
471    [9.9, ()=>{click();tema('neon',q('[data-tema] [data-t=neon]'));}],
472    [10.9,()=>{moveTo(q('[data-tema] [data-t=pulse]'));}],[11.4,()=>{click();tema('pulse',q('[data-tema] [data-t=pulse]'));}],
473    [12.1,()=>{hot('player');moveTo(q('[data-plr] [data-p=b]'));scene('Estilo do player','sem tocar em código');}],
474    [12.8,()=>{click();player('b',q('[data-plr] [data-p=b]'));}],
475    [13.7,()=>{moveTo(q('[data-plr] [data-p=a]'));}],[14.2,()=>{click();player('a',q('[data-plr] [data-p=a]'));}],
476    // ATO 2 — Conteúdo
477    [15.0,()=>{act('2','O site se enche de conteúdo');tab('conteudo');hot('');moveTo(q('[data-r=top]'));scene('Top Músicas','o ouvinte vota, o ranking se monta');}],
478    [15.8,()=>{click();mod('top');scrollTo(150);}],
479    [17.4,()=>{moveTo(q('[data-r=news]'));scene('Notícias','entram sozinhas, por RSS');}],
480    [18.2,()=>{click();mod('news');scrollTo(330);}],
481    [19.8,()=>{moveTo(q('[data-r=prog]'));scene('Programação','quem está no ar agora');}],
482    [20.6,()=>{click();mod('prog');scrollTo(490);}],
483    [22.0,()=>{moveTo(q('[data-r=pod]'));scene('Podcasts','feed pronto pro Spotify e Apple');}],
484    [22.8,()=>{click();mod('pod');scrollTo(620);}],
485    // ATO 3 — Monte a home (arrastar módulos)
486    [24.6,()=>{act('3','Monte a home do seu jeito');prev.classList.add('editing');scrollTo(300);scene('Arraste pra reordenar','os módulos, na ordem que quiser');}],
487    [25.8,()=>{moveTo(q('[data-mod=prog]'),.5,.2);}],
488    [26.7,()=>{click();lift('prog');}],
489    [27.5,()=>{moveMod('prog',35);moveTo(q('[data-mod=news]'),.5,.2);}],
490    [28.8,()=>{lift('prog',false);scene('O site já reflete','a nova ordem, na hora');}],
491    [30.2,()=>{prev.classList.remove('editing');}],
492    // ATO 4 — Envolver
493    [31.0,()=>{act('4','Engajamento e receita');tab('envolver');scrollTo(0);moveTo(q('[data-r=slider]'));scene('Slider de destaque','promoções em primeiro plano');}],
494    [31.8,()=>{click();mod('slider');q('[data-r=promo]').classList.add('on');}],
495    [33.4,()=>{scrollTo(770);moveTo(q('[data-r=banner]'));scene('Publicidade','espaço que vira receita');}],
496    [34.2,()=>{click();mod('banner');}],
497    [35.6,()=>{scrollTo(0);moveTo(q('[data-r=wa]'));scene('WhatsApp','o ouvinte fala em um toque');}],
498    [36.4,()=>{click();toggle('wa');}],
499    // ATO 5 — Ao vivo
500    [37.6,()=>{act('5','Ao vivo e no bolso');tab('aovivo');moveTo(q('[data-r=video]'));scene('Vídeo ao vivo','a câmera do estúdio no ar');}],
501    [38.4,()=>{click();toggle('video');}],
502    [40.6,()=>{q('[data-video]').classList.remove('show');moveTo(q('[data-r=pwa]'));scene('App instalável','o site vira app no celular');}],
503    [41.4,()=>{click();toggle('pwa');}],
504    // FINAL
505    [43.6,()=>{q('[data-pwa]').classList.remove('show');actEl.classList.remove('show');sceneEl.classList.remove('show');
506      q('[data-more]').classList.add('show');buildMore();}],
507  ];
508  const TOTAL=49;
509
510  const moreItems=[['shield','Segurança do stream'],['lock','LGPD e cookies'],['zap','SEO nativo'],['save','Backup automático'],
511    ['map','Mapa de ouvintes'],['users','Vários usuários'],['net','Modo Rede'],['img','Direitos de imagem'],['music','ID da música']];
512  const svg={shield:'<path d="M20 13c0 5-3.5 7.5-7.7 9a1 1 0 0 1-.6 0C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.2-2.7a1.2 1.2 0 0 1 1.6 0C14.5 3.8 17 5 19 5a1 1 0 0 1 1 1z"/>',
513    lock:'<rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>
513',zap:'<path d="M13 2 3 14h7l-1 8 10-12h-7z"/>',
514    save:'<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8M7 3v5h8"/>',
515    map:'<path d="M20 10c0 5-8 12-8 12s-8-7-8-12a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/>',users:'<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9"/>',
516    net:'<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3M12 12V8"/>',
517    img:'<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>',music:'<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>'};
518  function buildMore(){const g=root.querySelector('[data-moregrid]');if(g.childElementCount)return;
519    moreItems.forEach((it,i)=>{const el=document.createElement('span');el.className='g';el.style.animationDelay=(i*.07)+'s';
520      el.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">'+svg[it[0]]+'</svg>'+it[1];g.appendChild(el);});}
521
522  function reset(){
523    color('#FAC643',swBtns[0]);font(0.5);tema('pulse',root.querySelector('[data-tema] [data-t=pulse]'));player('a',root.querySelector('[data-plr] [data-p=a]'));
524    tab('aparencia');hot('');scrollTo(0);prev.classList.remove('editing');setOrder();
525    root.querySelectorAll('.mod[data-mod]').forEach(m=>m.classList.remove('show','lift'));
526    root.querySelectorAll('.row').forEach(r=>r.classList.remove('on'));
527    ['wa','pwa','video','more'].forEach(k=>root.querySelector('[data-'+k+']').classList.remove('show'));
528    moveTo(root.querySelector('.mod--hero'),.5,.4);
529  }
530  function play(){reset();L.forEach(([t,fn])=>setTimeout(fn,t*1000));
531    if(!reduce){progress.style.transition='none';progress.style.width='0';requestAnimationFrame(()=>{progress.style.transition=`width ${TOTAL}s linear`;progress.style.width='100%';});}
532    setTimeout(play,TOTAL*1000);}
533  if(reduce){tab('aparencia');mod('top');mod('news');mod('slider');toggle('wa');scene('Você mexe no painel','o site muda na hora');buildMore();}
534  else{const io=new IntersectionObserver(es=>es.forEach(e=>{if(e.isIntersecting){io.disconnect();play();}}),{threshold:.3});io.observe(root);}
535})();
536/* =====================================================================
537   VITRINE AO VIVO — clicar no logo do cliente e ouvir a rádio no ar.
538   Regras que valem ouro aqui:
539     · o <audio> NUNCA recebe src antes do clique (preload="none"): stream
540       carregado à toa entra como OUVINTE na estatística da rádio cliente;
541     · uma rádio por vez;
542     · a API é consultada pelo nosso /api/no-ar.php (lista-branca + cache),
543       nunca direto do navegador;
544     · o polling da música só roda com o player aberto e a aba visível.
545   ===================================================================== */
546(function () {
547  var faixa = document.querySelector('[data-cli]');
548  var dock  = document.querySelector('[data-dock]');
549  if (!faixa || !dock) { return; }
550
551  var audio  = dock.querySelector('[data-dock-audio]');
552  var elNome = dock.querySelector('[data-dock-nome]');
553  var elMus  = dock.querySelector('[data-dock-mus]');
554  var elLogo = dock.querySelector('[data-dock-logo]');
555  var elLive = dock.querySelector('.uh-dock__live');
556  var bMain  = dock.querySelector('[data-dock-toggle]');
557  var bClose = dock.querySelector('[data-dock-close]');
558  var vol    = dock.querySelector('[data-dock-vol]');
559  var dica   = document.querySelector('[data-cli-dica]');
560  var dicaTx = document.querySelector('[data-cli-txt]');
561
562  var atual = null;      // porta em reprodução
563  var timer = null;      // polling do "tocando agora"
564  var pedido = 0;        // serializa respostas fora de ordem
565
566  function api(qs) {
567    return fetch('api/no-ar.php?' + qs, { credentials: 'omit' })
568      .then(function (r) { return r.ok ? r.json() : null; })
569      .catch(function () { return null; });
570  }
571  function botoes(porta) {   // as 2 cópias do letreiro têm o mesmo data-radio
572    return faixa.querySelectorAll('[data-radio="' + porta + '"]');
573  }
574  function marcar(porta, cls, on) {
575    var l = botoes(porta), i;
576    for (i = 0; i < l.length; i++) { l[i].classList[on ? 'add' : 'remove'](cls); }
577  }
578  function limpar() {
579    var l = faixa.querySelectorAll('.is-playing,.is-loading'), i;
580    for (i = 0; i < l.length; i++) { l[i].classList.remove('is-playing', 'is-loading'); }
581    faixa.classList.remove('is-playing');
582  }
583
584  /* ── 1) quem está no ar: acende o pontinho verde nos logos ───────────── */
585  function acender() {
586    api('all=1').then(function (d) {
587      if (!d || !d.radios) { return; }
588      var p, n = 0;
589      for (p in d.radios) {
590        if (d.radios[p] && d.radios[p].no_ar) { marcar(p, 'is-live', true); n++; }
591      }
592      if (n && dica) {
593        dica.classList.add('on');
594        if (dicaTx) {
595          dicaTx.textContent = n === 1
596            ? 'Uma dessas rádios está no ar agora. Clique no logo e ouça.'
597            : n + ' dessas rádios estão no ar agora. Clique em um logo e ouça.';
598        }
599      }
600    });
601  }
602  // só quando a faixa aparece: nada de disparar 18 consultas em quem nem rolou até lá
603  if ('IntersectionObserver' in window) {
604    var io = new IntersectionObserver(function (es) {
605      if (es[0].isIntersecting) { io.disconnect(); acender(); }
606    }, { rootMargin: '200px' });
607    io.observe(faixa);
608  } else { acender(); }
609
610  /* ── 2) tocar ─────────────────────────────────────────────────────────── */
611  function parar() {
612    audio.pause();
613    audio.removeAttribute('src');
614    audio.load();                       // encerra a conexão com o streaming
615    clearInterval(timer); timer = null;
616    limpar();
617    dock.classList.remove('playing', 'loading');
618    atual = null;
619  }
620  function fechar() {
621    parar();
622    dock.classList.remove('show');
623    setTimeout(function () { if (!atual) { dock.hidden = true; } }, 500);
624  }
625
626  function tocar(btn) {
627    var porta = btn.getAttribute('data-radio');
628    if (porta === atual) {                       // clicou de novo = pausa/retoma
629      if (audio.paused) { audio.play().catch(function () {}); } else { audio.pause(); }
630      return;
631    }
632    parar();
633    atual = porta;
634    var meu = ++pedido;
635
636    elNome.textContent = btn.getAttribute('data-nome');
637    elLogo.src = btn.getAttribute('data-logo');
638    elLogo.alt = btn.getAttribute('data-nome');
639    elMus.textContent = '';
640    elLive.lastChild.nodeValue = 'CONECTANDO';
641    dock.classList.remove('off');
642    dock.hidden = false;
643    void dock.offsetWidth;                       // reflow p/ a transição de entrada rodar
644    dock.classList.add('show', 'loading');
645    faixa.classList.add('is-playing');
646    marcar(porta, 'is-loading', true);
647
648    api('p=' + encodeURIComponent(porta)).then(function (d) {
649      if (meu !== pedido) { return; }            // o visitante já clicou em outra
650      if (!d || !d.no_ar || !d.stream) {
651        dock.classList.remove('loading');
652        dock.classList.add('off');
653        elLive.lastChild.nodeValue = 'FORA DO AR';
654        elMus.textContent = 'Esta rádio não está transmitindo neste momento.';
655        marcar(porta, 'is-loading', false);
656        return;
657      }
658      elLive.lastChild.nodeValue = 'AO VIVO';
659      pintarMusica(d.musica);
660      audio.src = d.stream;                      // só agora a conexão é aberta
661      audio.volume = (vol ? vol.value : 80) / 100;
662      audio.play().catch(function () {
663        // autoplay barrado só acontece sem gesto; aqui houve clique. Se cair
664        // aqui é stream inacessível: some com o "carregando" em vez de girar eterno.
665        dock.classList.remove('loading');
666        marcar(porta, 'is-loading', false);
667      });
668      clearInterval(timer);
669      timer = setInterval(atualizarMusica, 20000);
670    });
671  }
672  function pintarMusica(m) { elMus.textContent = m ? '·  ' + m : ''; }
673  function atualizarMusica() {
674    if (!atual || document.hidden) { return; }
675    var meu = pedido;
676    api('p=' + encodeURIComponent(atual)).then(function (d) {
677      if (d && meu === pedido) { pintarMusica(d.musica); }
678    });
679  }
680
681  /* ── 3) eventos ───────────────────────────────────────────────────────── */
682  faixa.addEventListener('click', function (e) {
683    var b = e.target.closest('.uh-cli__logo--play');
684    if (b) { tocar(b); }
685  });
686  bMain.addEventListener('click', function () {
687    if (!atual) { return; }
688    if (audio.paused) { audio.play().catch(function () {}); } else { audio.pause(); }
689  });
690  bClose.addEventListener('click', fechar);
691  if (vol) {
692    vol.addEventListener('input', function () { audio.volume = vol.value / 100; });
693  }
694  audio.addEventListener('playing', function () {
695    dock.classList.remove('loading'); dock.classList.add('playing');
696    bMain.setAttribute('aria-label', 'Pausar');
697    if (atual) { marcar(atual, 'is-loading', false); marcar(atual, 'is-playing', true); }
698  });
699  audio.addEventListener('pause', function () {
700    dock.classList.remove('playing');
701    bMain.setAttribute('aria-label', 'Tocar');
702    if (atual) { marcar(atual, 'is-playing', false); }
703    faixa.classList.remove('is-playing');
704  });
705  audio.addEventListener('play', function () {
706    dock.classList.add('loading');
707    faixa.classList.add('is-playing');
708  });
709  audio.addEventListener('error', function () {
710    if (!audio.getAttribute('src')) { return; }   // src limpo pelo parar(): não é erro
711    dock.classList.remove('loading', 'playing');
712    dock.classList.add('off');
713    elLive.lastChild.nodeValue = 'SEM SINAL';
714    elMus.textContent = 'Não foi possível conectar agora.';
715    limpar();
716  });
717  document.addEventListener('keydown', function (e) {
718    if (e.key === 'Escape' && atual) { fechar(); }
719  });
720
721  // a tela de bloqueio/central de mídia do celular mostra a rádio, não "uhost.com.br"
722  if ('mediaSession' in navigator) {
723    audio.addEventListener('playing', function () {
724      try {
725        navigator.mediaSession.metadata = new MediaMetadata({
726          title: elMus.textContent.replace(/^·\s+/, '') || 'Ao vivo',
727          artist: elNome.textContent,
728          album: 'UHOST'
729        });
730      } catch (err) {}
731    });
732    try {
733      navigator.mediaSession.setActionHandler('play',  function () { audio.play().catch(function () {}); });
734      navigator.mediaSession.setActionHandler('pause', function () { audio.pause(); });
735      navigator.mediaSession.setActionHandler('stop',  fechar);
736    } catch (err) {}
737  }
738})();
739
740/* =====================================================================
741   DIAGNÓSTICO DA OPERAÇÃO DIGITAL (diagnostico.php)
742   Uma pergunta por vez, sem recarregar. O resultado aparece na hora —
743   sem cadastro, por decisão de marca (a /planos promete que a UHOST não
744   esconde coisa atrás de formulário; travar o diagnóstico desmentiria).
745   ===================================================================== */
746(function () {
747  var caixa = document.querySelector('[data-diag]');
748  if (!caixa || !window.UH_DIAG_REC) { return; }
749
750  var res    = document.querySelector('[data-diag-res]');
751  var barra  = caixa.querySelector('[data-diag-barra]');
752  var passo  = caixa.querySelector('[data-diag-passo]');
753  var voltar = caixa.querySelector('[data-diag-voltar]');
754  var perg   = caixa.querySelectorAll('[data-diag-p]');
755  var total  = perg.length;
756  var atual  = 0;
757  var notas  = [];
758
759  function mostrar(i) {
760    for (var k = 0; k < total; k++) { perg[k].hidden = (k !== i); }
761    passo.textContent = 'Pergunta ' + (i + 1) + ' de ' + total;
762    barra.style.width = ((i / total) * 100) + '%';
763    voltar.hidden = (i === 0);
764    var b = perg[i].querySelector('.uh-diag__o');
765    if (b) { b.focus({ preventScroll: true }); }
766  }
767
768  caixa.addEventListener('click', function (e) {
769    var b = e.target.closest('.uh-diag__o');
770    if (b) {
771      notas[atual] = parseInt(b.getAttribute('data-diag-v'), 10);
772      atual++;
773      if (atual < total) { mostrar(atual); } else { concluir(); }
774      return;
775    }
776    if (e.target.closest('[data-diag-voltar]') && atual > 0) { atual--; mostrar(atual); }
777  });
778
779  function concluir() {
780    var soma = 0, i;
781    for (i = 0; i < notas.length; i++) { soma += notas[i]; }
782    var nota = Math.round((soma / (total * 2)) * 100);
783
784    // faixa
785    var f = window.UH_DIAG_F[window.UH_DIAG_F.length - 1];
786    for (i = 0; i < window.UH_DIAG_F.length; i++) {
787      if (nota >= window.UH_DIAG_F[i][0] && nota <= window.UH_DIAG_F[i][1]) { f = window.UH_DIAG_F[i]; break; }
788    }
789    res.querySelector('[data-diag-faixa]').textContent = f[2];
790    res.querySelector('[data-diag-texto]').innerHTML   = f[3];
791
792    // as 3 recomendações mais urgentes: as respostas mais fracas primeiro,
793    // desempatando pela ordem das perguntas (as primeiras doem mais)
794    var ordem = [];
795    for (i = 0; i < notas.length; i++) { ordem.push([notas[i], i]); }
796    ordem.sort(function (a, b) { return a[0] - b[0] || a[1] - b[1]; });
797    var ul = res.querySelector('[data-diag-recs]');
798    ul.innerHTML = '';
799    var postas = 0;
800    for (i = 0; i < ordem.length && postas < 3; i++) {
801      if (ordem[i][0] >= 2) { continue; }              // já está bem: não vira tarefa
802      var li = document.createElement('li');
803      li.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="var(--uh-gold)" stroke-width="2.5" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><span>'
804                   + window.UH_DIAG_REC[ordem[i][1]] + '</span>';
805      ul.appendChild(li);
806      postas++;
807    }
808    if (!postas) {
809      var li2 = document.createElement('li');
810      li2.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="var(--uh-gold)" stroke-width="2.5" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><span>Nada urgente apareceu. Vale conversar sobre custo: operação nesse nível costuma estar espalhada em vários contratos.</span>';
811      ul.appendChild(li2);
812    }
813
814    // leva as respostas junto no envio, senão o lead chega sem contexto
815    var ini = res.querySelector('[data-diag-inota]');
816    if (ini) { ini.value = nota; }
817    var iresp = res.querySelector('[data-diag-iresp]');
818    if (iresp) {
819      var linhas = [];
820      for (i = 0; i < notas.length; i++) {
821        linhas.push((i + 1) + '. ' + window.UH_DIAG_Q[i] + '\n   -> ' + window.UH_DIAG_O[i][notas[i]]);
822      }
823      iresp.value = linhas.join('\n');
824    }
825
826    caixa.hidden = true;
827    res.hidden = false;
828    res.id = 'resultado';
829    res.scrollIntoView({ behavior: 'smooth', block: 'start' });
830
831    // conta a nota e desenha o arco
832    var reduz = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
833    var num = res.querySelector('[data-diag-num]');
834    var arco = res.querySelector('[data-diag-arco]');
835    var fim = 327 - (327 * nota / 100);
836    if (reduz) { num.textContent = nota; arco.style.strokeDashoffset = fim; return; }
837    arco.style.transition = 'stroke-dashoffset 1.2s cubic-bezier(.22,1,.36,1)';
838    requestAnimationFrame(function () { arco.style.strokeDashoffset = fim; });
839    var t0 = null;
840    requestAnimationFrame(function step(ts) {
841      if (!t0) { t0 = ts; }
842      var p = Math.min((ts - t0) / 1200, 1);
843      num.textContent = Math.round(nota * (1 - Math.pow(1 - p, 3)));
844      if (p < 1) { requestAnimationFrame(step); }
845    });
846  }
847
848  var rf = document.querySelector('[data-diag-refazer]');
849  if (rf) {
850    rf.addEventListener('click', function () {
851      atual = 0; notas = [];
852      res.hidden = true; caixa.hidden = false;
853      mostrar(0);
854      caixa.scrollIntoView({ behavior: 'smooth', block: 'center' });
855    });
856  }
857  mostrar(0);
858})();
859
860/* =====================================================================
861   BARRA FIXA DE CONVERSAO (uh_sticky)
862   Aparece depois que o visitante passa do hero, e SOME quando o CTA final
863   entra na tela (dois botões iguais na mesma dobra viram ruído) ou quando
864   o player da vitrine abre (quem está ouvindo não quer ser interrompido).
865   ===================================================================== */
866(function () {
867  var barra = document.querySelector('[data-sticky]');
868  if (!barra) { return; }
869
870  var ctaFinal = document.querySelector('.uh-cta');
871  var dock     = document.querySelector('[data-dock]');
872  var gatilho  = 620;                       // ~ o fim do hero
873  var tocando  = false;
874
875  barra.hidden = false;
876
877  /* 🚨 A visibilidade do CTA final é medida por GEOMETRIA, não por
878     IntersectionObserver. Nas duas páginas mais longas (/planos com 14.205 px
879     e /site-para-radio com 15.736) o observer disparava ANTES do layout
880     assentar — as imagens da vitrine ainda estavam carregando e empurravam a
881     página em ~190 px depois — e a barra ficava presa no estado errado, sem
882     nada que a reavaliasse. Medir no mesmo rAF do scroll não tem esse furo. */
883  function ctaVisivel() {
884    if (!ctaFinal) { return false; }
885    var r = ctaFinal.getBoundingClientRect();
886    var alturaJanela = window.innerHeight || document.documentElement.clientHeight;
887    return r.top < alturaJanela * 0.85 && r.bottom > 0;
888  }
889
890  function avaliar() {
891    var mostrar = (window.scrollY > gatilho) && !ctaVisivel() && !tocando;
892    barra.classList.toggle('show', mostrar);
893    document.body.classList.toggle('tem-sticky', mostrar);
894  }
895
896  var tick = false;
897  function agendar() {
898    if (!tick) { tick = true; requestAnimationFrame(function () { tick = false; avaliar(); }); }
899  }
900  window.addEventListener('scroll', agendar, { passive: true });
901  window.addEventListener('resize', agendar, { passive: true });
902  window.addEventListener('load',   agendar);   // imagens tardias mudam a altura
903
904  if (dock) {
905    // o dock não emite evento próprio: observa a classe que o player liga/desliga
906    new MutationObserver(function () {
907      tocando = dock.classList.contains('show');
908      avaliar();
909    }).observe(dock, { attributes: true, attributeFilter: ['class'] });
910  }
911
912  avaliar();
913})();

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.