PageSourceSearch

https://www.brasil247.com/wp-content/themes/tiebreak/assets/js/b247-sidebar.js?m=1789751254g

js brasil247.com collected 2026-09-24 08:28:26 UTC 27,482 bytes, 632 lines download raw bytes

1/**
2 * b247-sidebar.js
3 * Charges carousel + mobile deck video overlay.
4 */
5(function (w, d) {
6	'use strict';
7
8	/* ── Charges carousel ──────────────────────────────────────────────────── */
9	function initCarousel(el) {
10		var slides = el.querySelectorAll('.b247-charges-slide');
11		var dots   = el.querySelectorAll('.b247-charges-dot');
12		var prev   = el.querySelector('.b247-charges-prev');
13		var next   = el.querySelector('.b247-charges-next');
14		var total  = slides.length;
15		var cur    = 0;
16		var timer;
17
18		if (!total) return;
19		if (total === 1) { slides[0].classList.add('b247-charges-slide--active'); return; }
20
21		function show(n) {
22			slides[cur].classList.remove('b247-charges-slide--active');
23			if (dots[cur]) dots[cur].classList.remove('b247-charges-dot--active');
24			cur = (n + total) % total;
25			slides[cur].classList.add('b247-charges-slide--active');
26			if (dots[cur]) dots[cur].classList.add('b247-charges-dot--active');
27		}
28
29		function resetTimer() {
30			clearInterval(timer);
31			timer = setInterval(function () { show(cur + 1); }, 5000);
32		}
33
34		if (prev) prev.addEventListener('click', function () { show(cur - 1); resetTimer(); });
35		if (next) next.addEventListener('click', function () { show(cur + 1); resetTimer(); });
36
37		dots.forEach(function (dot, i) {
38			dot.addEventListener('click', function () { show(i); resetTimer(); });
39		});
40
41		var sx = 0;
42		el.addEventListener('touchstart', function (e) { sx = e.touches[0].clientX; }, { passive: true });
43		el.addEventListener('touchend', function (e) {
44			var dx = sx - e.changedTouches[0].clientX;
45			if (Math.abs(dx) > 30) { show(dx > 0 ? cur + 1 : cur - 1); resetTimer(); }
46		}, { passive: true });
47
48		show(0);
49		resetTimer();
50	}
51
52	/* ── Mobile deck video overlay ─────────────────────────────────────────── */
53	function initDeck() {
54		// The deck button is the original entry point; .b247-video-trigger lets
55		// any other surface (article channels box, homepage CTA) open the same
56		// overlay. One overlay, one player instance, whoever opens it.
57		var triggers  = d.querySelectorAll('#b247-deck-video-btn, .b247-video-trigger');
58		var overlay   = d.getElementById('b247-video-overlay');
59		var closeBtn  = d.getElementById('b247-video-overlay-close');
60		var container = d.getElementById('b247-video-container');
61		var loaded    = false;
62
63		if (!triggers.length || !overlay) return;
64
65		function openOverlay() {
66			overlay.removeAttribute('hidden');
67			d.body.style.overflow = 'hidden';
68			history.pushState({ b247Video: true }, '');
69			if (!loaded) {
70				loaded = true;
71				var s = d.createElement('script');
72				s.setAttribute('data-cfasync', 'false');
73				s.async = true;
74				s.src = 'https://go.trvdp.com/init/15165.js?vpid=11878';
75				if (container) container.appendChild(s);
76			}
77		}
78
79		function closeOverlay(skipHistory) {
80			if (overlay.hasAttribute('hidden')) return;
81			overlay.setAttribute('hidden', '');
82			d.body.style.overflow = '';
83			if (!skipHistory) history.back();
84		}
85
86		Array.prototype.forEach.call(triggers, function (t) {
87			t.addEventListener('click', openOverlay);
88		});
89		if (closeBtn) closeBtn.addEventListener('click', function () { closeOverlay(false); });
90
91		overlay.addEventListener('click', function (e) {
92			if (e.target === overlay) closeOverlay(false);
93		});
94
95		d.addEventListener('keydown', function (e) {
96			if (e.key === 'Escape' && !overlay.hasAttribute('hidden')) closeOverlay(false);
97		});
98
99		w.addEventListener('popstate', function () {
100			closeOverlay(true);
101		});
102	}
103
104	/* ── Scroll-hide header + deck ─────────────────────────────────────────── */
105	function initScrollHide() {
106		var lastY   = w.scrollY || w.pageYOffset || 0;
107		var ticking = false;
108
109		w.addEventListener('scroll', function () {
110			if (ticking) return;
111			// Never hide header while the nav overlay is open — hamburger must stay reachable.
112			if (d.querySelector('.wp-block-navigation__responsive-container.is-menu-open')) return;
113			ticking = true;
114			w.requestAnimationFrame(function () {
115				var y = w.scrollY || w.pageYOffset || 0;
116				if (y < 80) {
117					d.body.classList.remove('b247-scrolled-down');
118				} else if (y > lastY + 8) {
119					d.body.classList.add('b247-scrolled-down');
120				} else if (y < lastY - 8) {
121					d.body.classList.remove('b247-scrolled-down');
122				}
123				lastY   = y;
124				ticking = false;
125			});
126		}, { passive: true });
127	}
128
129	/* ── Search modal ──────────────────────────────────────────────────────── */
130	function initSearchModal() {
131		var btn     = d.getElementById('b247-search-btn');
132		var modal   = d.getElementById('b247-search-modal');
133		var closeBt = d.getElementById('b247-search-close');
134		var input   = d.getElementById('b247-search-input');
135
136		if (!btn || !modal) return;
137
138		function open() {
139			modal.removeAttribute('hidden');
140			d.body.style.overflow = 'hidden';
141			if (input) setTimeout(function () { input.focus(); }, 30);
142		}
143
144		function close() {
145			modal.setAttribute('hidden', '');
146			d.body.style.overflow = '';
147			btn.focus();
148		}
149
150		btn.addEventListener('click', open);
151		if (closeBt) closeBt.addEventListener('click', close);
152
153		modal.addEventListener('click', function (e) {
154			if (e.target === modal) close();
155		});
156
157		d.addEventListener('keydown', function (e) {
158			if (e.key === 'Escape' && !modal.hasAttribute('hidden')) close();
159		});
160	}
161
162	/* ── TV 247 schedule: highlight the program currently on air ───────────── */
163	function initTvSchedule() {
164		var list = d.querySelector('.b247-tv-schedule-list');
165		if (!list) return;
166
167		var items = list.querySelectorAll('.b247-tv-item[data-start]');
168		if (!items.length) return;
169
170		// The live player in the same widget. The program on air drives which
171		// video it shows; programs without their own video keep the default
172		// (channel live) embed captured here.
173		var liveFrame    = d.querySelector('.b247-live-embed-frame');
174		var defaultEmbed = liveFrame
175			? (liveFrame.getAttribute('data-src') || liveFrame.getAttribute('src') || '')
176			: '';
177
178		// Point the live iframe at the given embed URL. Updates data-src while the
179		// frame is still deferred (initLiveEmbed promotes it later), or src once
180		// it has loaded — only when the URL actually changes, so it doesn't reload
181		// mid-program.
182		function applyLiveVideo(idx) {
183			if (!liveFrame) return;
184			var embed = (idx >= 0 && items[idx].getAttribute('data-embed'))
185				? items[idx].getAttribute('data-embed')
186				: defaultEmbed;
187			if (!embed) return;
188			if (liveFrame.hasAttribute('data-src')) {
189				if (liveFrame.getAttribute('data-src') !== embed) liveFrame.setAttribute('data-src', embed);
190			} else if (liveFrame.getAttribute('src') !== embed) {
191				liveFrame.setAttribute('src', embed);
192			}
193		}
194
195		// Current time in Brazil, independent of the visitor's own timezone,
196		// so the highlight is right even on edge-cached HTML.
197		function nowMinutes() {
198			try {
199				// Build the formatter once (cached on the fn) — constructing it per
200				// 60s tick is 5-20ms on low-end mobile and can land in a tap's INP window.
201				var fmt = nowMinutes._fmt || (nowMinutes._fmt = new Intl.DateTimeFormat('en-GB', {
202					timeZone: 'America/Sao_Paulo',
203					hour: '2-digit',
204					minute: '2-digit',
205					hour12: false
206				}));
207				var parts = fmt.format(new Date()).split(':');
208				return (parseInt(parts[0], 10) % 24) * 60 + parseInt(parts[1], 10);
209			} catch (e) {
210				var n = new Date();
211				return n.getHours() * 60 + n.getMinutes();
212			}
213		}
214
215		function update() {
216			var mins = nowMinutes();
217			var liveIndex = -1;
218			for (var i = 0; i < items.length; i++) {
219				var start = parseInt(items[i].getAttribute('data-start'), 10);
220				var next = (i + 1 < items.length)
221					? parseInt(items[i + 1].getAttribute('data-start'), 10)
222					: 24 * 60;
223				if (mins >= start && mins < next) liveIndex = i;
224			}
225			for (var j = 0; j < items.length; j++) {
226				items[j].classList.toggle('is-live', j === liveIndex);
227			}
228			applyLiveVideo(liveIndex);
229		}
230
231		update();
232		w.setInterval(update, 60000);
233	}
234
235	/* ── Ao vivo embed: defer the YouTube iframe until it nears the viewport ─ */
236	function initLiveEmbed() {
237		var frames = d.querySelectorAll('.b247-live-embed-frame[data-src]');
238		if (!frames.length) return;
239
240		function load(frame) {
241			var src = frame.getAttribute('data-src');
242			if (!src) return;
243			frame.setAttribute('src', src);
244			frame.removeAttribute('data-src');
245		}
246
247		if (!('IntersectionObserver' in w)) {
248			frames.forEach(load);
249			return;
250		}
251
252		var io = new IntersectionObserver(function (entries) {
253			entries.forEach(function (entry) {
254				if (entry.isIntersecting) {
255					load(entry.target);
256					io.unobserve(entry.target);
257				}
258			});
259		}, { rootMargin: '300px' });
260
261		frames.forEach(function (frame) { io.observe(frame); });
262	}
263
264	/* ── Ao vivo player: TV 247 / Cortes 247 toggle ────────────────────────── */
265	/*
266	 * Swaps the visible panel and STOPS the hidden one — symmetric for both the
267	 * live stream and the Cortes playlist. There's no reliable way to "pause" a
268	 * cross-origin YouTube iframe without the IFrame Player API, so we use the
269	 * standard trick: clear the hidden frame's src (stashing the URL in
270	 * data-paused-src) to stop playback, and restore it when its tab reopens.
271	 * The live frame's element is kept (only its src is cleared) so its existing
272	 * lazy-load + on-air program swap keep working; if that swap reloads it while
273	 * hidden it comes back muted (autoplay=1&mute=1), i.e. silent.
274	 */
275	function initLiveToggle() {
276		var toggles = d.querySelectorAll('.b247-live-toggle');
277		if (!toggles.length) return;
278
279		// Restore a frame's URL from whichever stash holds it: the Cortes lazy
280		// stash (first open) or the paused stash (re-open). Leaves an unloaded
281		// live frame's data-src alone so initLiveEmbed still promotes it.
282		function showFrame(frame) {
283			if (!frame) return;
284			var url = frame.getAttribute('data-cortes-src') || frame.getAttribute('data-paused-src');
285			if (url && frame.getAttribute('src') !== url) frame.setAttribute('src', url);
286			frame.removeAttribute('data-cortes-src');
287			frame.removeAttribute('data-paused-src');
288		}
289
290		// Stop playback by clearing src, stashing the URL so it can be restored.
291		function hideFrame(frame) {
292			if (!frame) return;
293			var cur = frame.getAttribute('src');
294			if (cur) {
295				frame.setAttribute('data-paused-src', cur);
296				frame.setAttribute('src', '');
297			}
298		}
299
300		toggles.forEach(function (toggle) {
301			var scope = toggle.parentNode;
302			if (!scope) return;
303			var btns = toggle.querySelectorAll('.b247-live-toggle-btn');
304
305			toggle.addEventListener('click', function (e) {
306				var btn = e.target.closest ? e.target.closest('.b247-live-toggle-btn') : null;
307				if (!btn) return;
308				var tab = btn.getAttribute('data-live-tab');
309
310				btns.forEach(function (b) {
311					var active = b === btn;
312					b.classList.toggle('is-active', active);
313					b.setAttribute('aria-pressed', active ? 'true' : 'false');
314				});
315
316				scope.querySelectorAll('.b247-live-panel').forEach(function (panel) {
317					var show = panel.getAttribute('data-live-panel') === tab;
318					panel.hidden = !show;
319					var frame = panel.querySelector('iframe');
320					if (show) { showFrame(frame); } else { hideFrame(frame); }
321				});
322			});
323		});
324	}
325
326	/* ── Hamburger menu enhancement ────────────────────────────────────────── */
327	/*
328	 * Injects a brand header, search shortcut, and footer (Apoio CTA + socials)
329	 * into the core navigation overlay the first time it opens. Keeps the WP
330	 * markup intact — the user still edits the menu in Site Editor — while
331	 * giving the overlay a magazine front-page feel.
332	 */
333	function initNavOverlay() {
334		var nav = d.querySelector('.b247-nav-hamburger');
335		if (!nav) return;
336		var container = nav.querySelector('.wp-block-navigation__responsive-container');
337		var content   = nav.querySelector('.wp-block-navigation__responsive-container-content');
338		if (!container || !content) return;
339
340		function createTextEl(tag, className, text) {
341			var el = d.createElement(tag);
342			if (className) el.className = className;
343			el.textContent = text;
344			return el;
345		}
346
347		function createSvg(pathData) {
348			var svg = d.createElementNS('http://www.w3.org/2000/svg', 'svg');
349			var path = d.createElementNS('http://www.w3.org/2000/svg', 'path');
350			svg.setAttribute('viewBox', '0 0 24 24');
351			svg.setAttribute('aria-hidden', 'true');
352			svg.setAttribute('focusable', 'false');
353			path.setAttribute('d', pathData);
354			svg.appendChild(path);
355			return svg;
356		}
357
358		function createSocialLink(href, label, pathData) {
359			var link = d.createElement('a');
360			link.href = href;
361			link.target = '_blank';
362			link.rel = 'noopener';
363			link.setAttribute('aria-label', label);
364			link.appendChild(createSvg(pathData));
365			return link;
366		}
367
368		var enriched = false;
369		function enrich() {
370			if (enriched) return;
371			enriched = true;
372
373			// ── Top: brand mark + search shortcut ────────────────────────
374			var top = d.createElement('div');
375			top.className = 'b247-nav-top';
376
377			var search = d.createElement('button');
378			search.type = 'button';
379			search.className = 'b247-nav-search';
380			search.setAttribute('data-action', 'open-search');
381			search.appendChild(createSvg('M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'));
382			search.appendChild(createTextEl('span', '', 'Buscar no 247'));
383			top.appendChild(search);
384
385			var apoioTop = d.createElement('a');
386			apoioTop.className = 'b247-nav-apoio b247-nav-apoio--top';
387			apoioTop.href = '/apoio/';
388			apoioTop.appendChild(createTextEl('span', '', 'Apoiar o 247'));
389			var arrow = d.createElement('span');
390			arrow.setAttribute('aria-hidden', 'true');
391			arrow.textContent = '→';
392			apoioTop.appendChild(arrow);
393			top.appendChild(apoioTop);
394
395			var quemSomos = d.createElement('a');
396			quemSomos.className = 'b247-nav-quem-somos';
397			quemSomos.href = '/quem-somos/';
398			quemSomos.appendChild(createTextEl('span', '', 'Quem somos'));
399			top.appendChild(quemSomos);
400
401			content.insertBefore(top, content.firstChild);
402
403			// Wire the search shortcut to the existing search modal.
404			search.addEventListener('click', function () {
405				// Closing the menu first lets the search modal own focus cleanly.
406				var closeBtn = nav.querySelector('.wp-block-navigation__responsive-container-close');
407				if (closeBtn) closeBtn.click();
408				setTimeout(function () {
409					var s = d.getElementById('b247-search-btn');
410					if (s) s.click();
411				}, 80);
412			});
413
414			// ── Bottom: Apoio CTA + social pills ─────────────────────────
415			var bottom = d.createElement('div');
416			bottom.className = 'b247-nav-bottom';
417			var apoio = d.createElement('a');
418			apoio.className = 'b247-nav-apoio';
419			apoio.href = '/apoio/';
420			apoio.appendChild(createTextEl('span', '', 'Apoiar o 247'));
421			var arrow = createTextEl('span', '', String.fromCharCode(0x2192));
422			arrow.setAttribute('aria-hidden', 'true');
423			apoio.appendChild(arrow);
424			bottom.appendChild(apoio);
425
426			var socials = d.createElement('div');
427			socials.className = 'b247-nav-socials';
428			socials.setAttribute('aria-label', 'Redes sociais');
429			socials.appendChild(createSocialLink('https://www.youtube.com/c/brasil247?sub_confirmation=1', 'YouTube', 'M23.5 6.2a3 3 0 0 0-2.1-2.1C19.6 3.6 12 3.6 12 3.6s-7.6 0-9.4.5a3 3 0 0 0-2.1 2.1C0 8.1 0 12 0 12s0 3.9.5 5.8a3 3 0 0 0 2.1 2.1c1.9.5 9.4.5 9.4.5s7.6 0 9.4-.5a3 3 0 0 0 2.1-2.1c.5-1.9.5-5.8.5-5.8s0-3.9-.5-5.8zM9.5 15.6V8.4L15.8 12l-6.3 3.6z'));
430			socials.appendChild(createSocialLink('https://x.com/brasil247', 'X', 'M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.737-8.835L1.254 2.25H8.08l4.258 5.635 5.906-5.635z'));
431			socials.appendChild(createSocialLink('https://www.facebook.com/brasil247', 'Facebook', 'M24 12.07C24 5.44 18.63.07 12 .07S0 5.44 0 12.07c0 6 4.39 10.95 10.13 11.85v-8.38H7.08v-3.47h3.05V9.43c0-3 1.79-4.67 4.53-4.67 1.31 0 2.69.24 2.69.24v2.95H15.83c-1.49 0-1.96.93-1.96 1.87v2.25h3.33l-.53 3.47h-2.8v8.38C19.61 23.02 24 18.06 24 12.07z'));
432			socials.appendChild(createSocialLink('https://instagram.com/brasil_247', 'Instagram', 'M12 2.16c3.2 0 3.58.01 4.85.07 3.25.15 4.77 1.69 4.92 4.92.06 1.27.07 1.65.07 4.85 0 3.2-.01 3.58-.07 4.85-.15 3.23-1.66 4.77-4.92 4.92-1.27.06-1.65.07-4.85.07s-3.58-.01-4.85-.07c-3.26-.15-4.77-1.7-4.92-4.92-.06-1.27-.07-1.64-.07-4.85 0-3.2.01-3.58.07-4.85.15-3.23 1.66-4.77 4.92-4.92C8.42 2.17 8.8 2.16 12 2.16zm0-2.16C8.74 0 8.33.01 7.05.07 2.7.27.27 2.69.07 7.05.01 8.33 0 8.74 0 12c0 3.26.01 3.67.07 4.95.2 4.36 2.62 6.78 6.98 6.98 1.28.06 1.69.07 4.95.07s3.67-.01 4.95-.07c4.35-.2 6.78-2.62 6.98-6.98.06-1.28.07-1.69.07-4.95s-.01-3.67-.07-4.95c-.2-4.35-2.62-6.78-6.98-6.98C15.67.01 15.26 0 12 0zm0 5.84a6.16 6.16 0 1 0 0 12.32 6.16 6.16 0 0 0 0-12.32zm0 10.16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6.41-11.85a1.44 1.44 0 1 0 0 2.88 1.44 1.44 0 0 0 0-2.88z'));
433			socials.appendChild(createSocialLink('https://t.me/brasil247oficial', 'Telegram', 'M11.94 0a12 12 0 1 0 0 24 12 12 0 0 0 0-24zm5 7.22c.1 0 .32.02.47.14a.5.5 0 0 1 .17.33c.02.09.04.3.02.47-.18 1.9-.96 6.5-1.36 8.63-.17.9-.5 1.2-.82 1.23-.7.07-1.22-.46-1.9-.9-1.06-.7-1.66-1.13-2.68-1.8-1.19-.78-.42-1.2.26-1.91.17-.18 3.25-2.98 3.3-3.23.01-.03.02-.15-.05-.21s-.18-.04-.25-.02c-.1.02-1.8 1.14-5.06 3.34-.48.33-.92.49-1.3.48-.43 0-1.26-.24-1.87-.44-.75-.25-1.34-.38-1.3-.79.03-.21.32-.44.9-.66 3.5-1.53 5.83-2.53 7-3.02 3.33-1.39 4.02-1.63 4.47-1.64z'));
434			bottom.appendChild(socials);
435
436			var footer = d.createElement('div');
437			footer.className = 'b247-nav-footer-links';
438			var about = d.createElement('a');
439			about.href = '/quem-somos';
440			about.textContent = 'Quem somos';
441			footer.appendChild(about);
442			var privacy = d.createElement('a');
443			privacy.href = '/politica-de-privacidade';
444			privacy.textContent = 'Privacidade';
445			footer.appendChild(privacy);
446			bottom.appendChild(footer);
447			content.appendChild(bottom);
448		}
449
450		// Anchor the dropdown panel directly below the header so the header (and
451		// the hamburger) never move when the menu opens. Measure the FIXED bar
452		// that holds the hamburger: on mobile that's .b247-header-shell (fixed),
453		// on desktop it's .b247-header-brand (fixed; the shell scrolls). Drop the
454		// scroll-hide class first so the bar is at the top when measured, and
455		// clamp to >= 0 so the panel can never anchor off-screen.
456		function setMenuTop() {
457			d.body.classList.remove('b247-scrolled-down');
458			// Defer the layout read (getBoundingClientRect) + property write to the
459			// next frame so opening the menu doesn't force a synchronous reflow of
460			// the whole listing inside the tap's INP window. The panel anchors one
461			// frame later — imperceptible.
462			w.requestAnimationFrame(function () {
463				var bar = nav.closest('.b247-header-brand') || d.querySelector('.b247-header-brand') || d.querySelector('.b247-header-shell');
464				var top = bar ? Math.max(0, Math.round(bar.getBoundingClientRect().bottom)) : 56;
465				container.style.setProperty('--b247-menu-top', top + 'px');
466			});
467		}
468
469		// Collapse every section so the menu always opens tidy (collapse-by-default).
470		function collapseAccordion() {
471			var open = content.querySelectorAll('.b247-acc-open');
472			for (var j = 0; j < open.length; j++) {
473				open[j].classList.remove('b247-acc-open');
474				var ic = open[j].querySelector('.wp-block-navigation__submenu-icon');
475				if (ic) ic.setAttribute('aria-expanded', 'false');
476			}
477		}
478
479		// Click accordion: each parent section toggles its own submenu open.
480		// Capture-phase + stopImmediatePropagation so the core nav block's own
481		// submenu handler never fires — we fully own the expand/collapse.
482		function wireAccordion() {
483			var parents = content.querySelectorAll('.wp-block-navigation-item.has-child');
484			for (var i = 0; i < parents.length; i++) {
485				(function (li) {
486					if (li.getAttribute('data-b247-acc')) return;
487					li.setAttribute('data-b247-acc', '1');
488					// The chevron may be a direct child of the <li> or nested inside
489					// the parent's content link depending on the core render path —
490					// a descendant lookup matches either.
491					var icon = li.querySelector('.wp-block-navigation__submenu-icon');
492					var label = li.querySelector(':scope > .wp-block-navigation-item__content');
493					function toggle(e) {
494						e.preventDefault();
495						e.stopImmediatePropagation();
496						var isOpen = li.classList.toggle('b247-acc-open');
497						if (icon) icon.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
498					}
499					if (label) label.addEventListener('click', toggle, true);
500					if (icon) icon.addEventListener('click', toggle, true);
501				})(parents[i]);
502			}
503		}
504
505		// Keep the panel anchored below the header across orientation/resize.
506		window.addEventListener('resize', function () {
507			if (container.classList.contains('is-menu-open')) setMenuTop();
508		});
509
510		// MutationObserver catches the moment WP toggles is-menu-open on the
511		// container — at that point the contents are real DOM and we can enrich.
512		var mo = new MutationObserver(function () {
513			if (container.classList.contains('is-menu-open')) {
514				enrich();
515				// Tag the navigation list so CSS can target it specifically — the
516				// core block doesn't ship a stable hook for the open state inside
517				// the <ul> tree.
518				var ul = nav.querySelector('.wp-block-navigation__container');
519				if (ul) ul.classList.add('b247-nav-open');
520				setMenuTop();
521				wireAccordion();
522				collapseAccordion();
523			}
524		});
525		mo.observe(container, { attributes: true, attributeFilter: ['class'] });
526
527		// When the menu opens, ensure the header is visible (undo scroll-hide)
528		// so the X close button is always reachable.
529		var navMo = new MutationObserver(function () {
530			if (container.classList.contains('is-menu-open')) {
531				d.body.classList.remove('b247-scrolled-down');
532			}
533		});
534		navMo.observe(container, { attributes: true, attributeFilter: ['class'] });
535
536		// Click outside the panel (on the dim scrim) closes the menu. Core wraps
537		// the content in __responsive-dialog / __responsive-close layers that fill
538		// the scrim, so a target===container check never matches; instead close
539		// whenever the click is NOT inside the content card. Ignore the close
540		// button itself so its own (re-dispatched) click can't loop.
541		container.addEventListener('click', function (e) {
542			if (!container.classList.contains('is-menu-open')) return;
543			if (e.target.closest('.wp-block-navigation__responsive-container-content')) return;
544			if (e.target.closest('.wp-block-navigation__responsive-container-close')) return;
545			var closeBtn = container.querySelector('.wp-block-navigation__responsive-container-close');
546			if (closeBtn) closeBtn.click();
547		});
548
549		// Some browsers paint with is-menu-open already set (back-button navigation).
550		if (container.classList.contains('is-menu-open')) {
551			enrich();
552			setMenuTop();
553			wireAccordion();
554			collapseAccordion();
555		}
556	}
557
558	/* ── Hamburger toggle — make open-btn also close the menu ─────────────── */
559	// The WP nav block uses two separate buttons (open / close). With the CSS
560	// X animation the open-btn is visible above the overlay when the menu is
561	// open; intercept those clicks and delegate to the close button so the
562	// single button works as a true toggle.
563	function initHamburgerToggle() {
564		var nav = d.querySelector('.b247-nav-hamburger');
565		if (!nav) return;
566		var openBtn   = nav.querySelector('.wp-block-navigation__responsive-container-open');
567		var container = nav.querySelector('.wp-block-navigation__responsive-container');
568		if (!openBtn || !container) return;
569
570		// Whether the menu was open when the finger landed. The click alone
571		// cannot be trusted: core's overlay closes itself on focusout when
572		// focus leaves for nowhere (relatedTarget null), which is what a tap
573		// on a button does on iOS, where buttons take no focus. So a tap on
574		// the X closed the menu before the click arrived, the click found it
575		// closed, this handler stepped aside, and core's open handler put the
576		// menu straight back — the reopen-on-close stutter. Latched on
577		// pointerdown (touchstart/mousedown for older engines), cleared soon
578		// after so an abandoned press cannot swallow a later keyboard click.
579		var wasOpen = false;
580		var latchTimer = 0;
581		function latch() {
582			wasOpen = container.classList.contains('is-menu-open');
583			clearTimeout(latchTimer);
584			latchTimer = setTimeout(function () { wasOpen = false; }, 800);
585		}
586		openBtn.addEventListener('pointerdown', latch, true);
587		openBtn.addEventListener('touchstart', latch, { capture: true, passive: true });
588		openBtn.addEventListener('mousedown', latch, true);
589
590		openBtn.addEventListener('click', function (e) {
591			var closing = wasOpen || container.classList.contains('is-menu-open');
592			wasOpen = false;
593			if (!closing) return;
594			e.preventDefault();
595			e.stopImmediatePropagation();
596			if (!container.classList.contains('is-menu-open')) return; // focusout already closed it
597			var closeBtn = container.querySelector('.wp-block-navigation__responsive-container-close');
598			if (closeBtn) closeBtn.click();
599		}, true); // capture phase — runs before WP's own handler
600	}
601
602	/* ── Boot ──────────────────────────────────────────────────────────────── */
603	function boot() {
604		// Attach interactive handlers immediately — user may tap right after paint.
605		initDeck();
606		initScrollHide();
607		initSearchModal();
608		initTvSchedule();
609		initNavOverlay();
610		initHamburgerToggle();
611		initLiveEmbed();
612		initLiveToggle();
613
614		// Carousel sets up listeners + a 5s setInterval; defer to idle so it
615		// doesn't compete with LCP work or first input.
616		var initCarousels = function () {
617			d.querySelectorAll('.b247-charges-carousel').forEach(initCarousel);
618		};
619		if ('requestIdleCallback' in w) {
620			w.requestIdleCallback(initCarousels, { timeout: 2000 });
621		} else {
622			setTimeout(initCarousels, 200);
623		}
624	}
625
626	if (d.readyState === 'loading') {
627		d.addEventListener('DOMContentLoaded', boot);
628	} else {
629		boot();
630	}
631
632}(window, document));

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.