PageSourceSearch

https://spyhop.org/wp-content/themes/spyshop-1/js/main.js?ver=1790764233

js spyhop.org collected 2026-10-01 13:05:08 UTC 54,590 bytes, 1,919 lines download raw bytes

1/* global Swiper, Fancybox, Lenis */
2(function ($) {
3	"use strict";
4
5	function swiperClickToSlide(swiper) {
6		if (!swiper || !swiper.el) return;
7		$(swiper.el).on("click", function (e) {
8			if ($(e.target).closest("a, button, input, textarea, select, .swiper-scrollbar").length) return;
9			var offset = e.pageX - $(this).offset().left;
10			var width = $(this).outerWidth();
11			if (offset < width * 0.35) {
12				swiper.slidePrev();
13			} else if (offset > width * 0.65) {
14				swiper.slideNext();
15			}
16		});
17	}
18
19	/* ============================================================
20	   ANNOUNCEMENT SWIPER
21	   ============================================================ */
22
23	/* ============================================================
24	   STICKY HEADER — hide top-bar on scroll
25	   ============================================================ */
26	function initStickyHeader() {
27		var topBar = document.querySelector(".site-header__top-bar");
28		if (!topBar) return;
29
30		var origH = topBar.offsetHeight;
31		topBar.style.height = origH + "px";
32
33		var isScrolled = false;
34
35		window.addEventListener(
36			"scroll",
37			function () {
38				var scrollY = window.scrollY || window.pageYOffset;
39				var shouldScroll = scrollY > 10;
40				if (shouldScroll === isScrolled) return;
41				isScrolled = shouldScroll;
42
43				if (isScrolled) {
44					topBar.style.height = "0";
45					topBar.style.paddingTop = "0";
46					topBar.style.paddingBottom = "0";
47					topBar.style.opacity = "0";
48				} else {
49					topBar.style.height = origH + "px";
50					topBar.style.paddingTop = "";
51					topBar.style.paddingBottom = "";
52					topBar.style.opacity = "";
53				}
54			},
55			{ passive: true },
56		);
57	}
58
59	/* ============================================================
60	   LENIS SMOOTH SCROLL
61	   ============================================================ */
62	function initLenis() {
63		if (typeof Lenis === "undefined") return;
64
65		var lenis = new Lenis({
66			autoRaf: true,
67		});
68
69		// Forward Lenis scroll position to anchor links
70		$(document).on("click", 'a[href^="#"]', function (e) {
71			var target = $(this).attr("href");
72			if (target === "#") return;
73			var $target = $(target);
74			if (!$target.length) return;
75			e.preventDefault();
76			lenis.scrollTo($target[0]);
77		});
78	}
79
80	/* ============================================================
81	   MEGA MENU
82	   ============================================================ */
83	function initMegaMenu() {
84		var $navItems = $(".nav-item--has-mega");
85		if (!$navItems.length) return;
86
87		var hideTimer = null;
88
89		function closeAll() {
90			$navItems
91				.find(".mega-menu")
92				.removeClass("mega-menu--open")
93				.attr("aria-hidden", "true");
94		}
95
96		$navItems.each(function () {
97			var $item = $(this);
98			var $menu = $item.find(".mega-menu");
99
100			if (!$menu.length) return;
101
102			function open() {
103				clearTimeout(hideTimer);
104				closeAll();
105				$menu.addClass("mega-menu--open").attr("aria-hidden", "false");
106			}
107
108			function scheduleClose() {
109				hideTimer = setTimeout(function () {
110					$menu
111						.removeClass("mega-menu--open")
112						.attr("aria-hidden", "true");
113				}, 120);
114			}
115
116			$item.on("mouseenter", open).on("mouseleave", scheduleClose);
117
118			$menu
119				.on("mouseenter", function () {
120					clearTimeout(hideTimer);
121				})
122				.on("mouseleave", scheduleClose);
123		});
124
125		$(document)
126			.on("keydown", function (e) {
127				if (e.key === "Escape") closeAll();
128			})
129			.on("click", function (e) {
130				if (!$(e.target).closest(".nav-item--has-mega").length) {
131					closeAll();
132				}
133			});
134	}
135
136	/* ============================================================
137	   MOBILE MENU
138	   ============================================================ */
139	function initMobileMenu() {
140		var $header = $(".site-header");
141		var $burger = $(".site-header__burger");
142		var $menu = $("#mobileMenu");
143
144		if (!$header.length || !$burger.length || !$menu.length) return;
145
146		function openMenu() {
147			$header.addClass("site-header--open");
148			$menu.attr("aria-hidden", "false");
149			$burger.attr("aria-expanded", "true");
150			$("body").addClass("scroll-locked");
151		}
152
153		function closeMenu() {
154			$header.removeClass("site-header--open");
155			$menu.attr("aria-hidden", "true");
156			$burger.attr("aria-expanded", "false");
157			$("body").removeClass("scroll-locked");
158		}
159
160		$burger.on("click", openMenu);
161		$("#mobileClose").on("click", closeMenu);
162		$("#menuOverlay").on("click", closeMenu);
163
164		$(document).on("keydown", function (e) {
165			if (e.key === "Escape" && $header.hasClass("site-header--open")) {
166				closeMenu();
167				$burger.trigger("focus");
168			}
169		});
170
171		$menu.on("click", ".site-header__mobile-trigger", function () {
172			var $trigger = $(this);
173			var $item = $trigger.closest(".site-header__mobile-item");
174			var isOpen = $item.hasClass("site-header__mobile-item--is-open");
175
176			$menu
177				.find(".site-header__mobile-item--is-open")
178				.removeClass("site-header__mobile-item--is-open")
179				.find(".site-header__mobile-trigger")
180				.attr("aria-expanded", "false");
181
182			if (!isOpen) {
183				$item.addClass("site-header__mobile-item--is-open");
184				$trigger.attr("aria-expanded", "true");
185			}
186		});
187	}
188
189	/* ============================================================
190	   ANNOUNCEMENT SWIPER
191	   ============================================================ */
192	function initAnnouncementSwiper() {
193		if (!$("#announcementSwiper").length) return;
194		new Swiper("#announcementSwiper", {
195			loop: true,
196			autoplay: { delay: 4000, disableOnInteraction: false },
197			allowTouchMove: false,
198			effect: "fade",
199			fadeEffect: { crossFade: true },
200			navigation: {
201				prevEl: ".site-header__top-arrow--prev",
202				nextEl: ".site-header__top-arrow--next",
203			},
204		});
205	}
206
207	/* ============================================================
208	   NEWS SWIPER
209	   ============================================================ */
210	function initNewsSwiper() {
211		var baseConfig = {
212			slidesPerView: 1.2,
213			centeredSlides: true,
214			loop: true,
215			spaceBetween: 0,
216			grabCursor: true,
217			scrollbar: { el: ".news-swiper__scrollbar", draggable: true },
218			breakpoints: {
219				769: {
220					slidesPerView: "auto",
221					centeredSlides: true,
222					loop: true,
223					loopedSlides: 8,
224				},
225			},
226		};
227		if ($("#newsSwiper").length) {
228			var newsSwiper = new Swiper("#newsSwiper", $.extend({}, baseConfig, {
229				loopedSlides: $("#newsSwiper .swiper-slide").length,
230			}));
231			swiperClickToSlide(newsSwiper);
232			setTimeout(function () {
233				newsSwiper.slideNext();
234			}, 600);
235		}
236		$(
237			"#communityNewsSwiper, #blogSingleSwiper, #relatedProgramsSwiper, #programsSwiper",
238		).each(function () {
239			var slideCount = $(this).find(".swiper-slide").length;
240			var swiper = new Swiper(this, $.extend({}, baseConfig, {
241				loopedSlides: slideCount,
242			}));
243			swiperClickToSlide(swiper);
244		});
245	}
246
247	/* ============================================================
248	   EVENTS SWIPER
249	   ============================================================ */
250	function initEventsSwiper() {
251		if (!$("#eventsSwiper").length) return;
252		var swiper = new Swiper("#eventsSwiper", {
253			slidesPerView: 1.15,
254			spaceBetween: 0,
255			grabCursor: true,
256			scrollbar: { el: ".programs-swiper__scrollbar", draggable: true },
257			breakpoints: { 769: { slidesPerView: 2.8 } },
258		});
259		swiperClickToSlide(swiper);
260	}
261		function initprogramssliderSwiper() {
262			if (!$('#section-programs-sliderSwiper').length) return
263			var swiper = new Swiper('#section-programs-sliderSwiper', {
264				slidesPerView: 1.2,
265				centeredSlides: false,
266				spaceBetween: 0,
267				grabCursor: true,
268				scrollbar: {
269					el: '.news-swiper__scrollbar',
270					draggable: true,
271				},
272			breakpoints: {
273				769: {
274					slidesPerView: 2.8,
275					centeredSlides: false,
276				},
277			},
278		})
279		swiperClickToSlide(swiper)
280	}
281	function initsingleeventsNewsSwiper(){
282			if (!$('#singleeventsNewsSwiper').length) return
283			var swiper = new Swiper('#singleeventsNewsSwiper', {
284				slidesPerView: 1.2,
285				centeredSlides: false,
286				spaceBetween: 0,
287				grabCursor: true,
288				scrollbar: {
289					el: '.news-swiper__scrollbar',
290					draggable: true,
291				},
292				breakpoints: {
293					769: {
294						slidesPerView: 2.8,
295						centeredSlides: false,
296					},
297				},
298			})
299			swiperClickToSlide(swiper)
300	}
301	/* ============================================================
302	   PROGRAMS SWIPER + PERSONA DROPDOWN
303	   ============================================================ */
304	function initProgramsSwiper() {
305		var $section = $(".programs");
306		if (!$section.length) return;
307
308		var $swiperEls = $section.find(".programs-swiper");
309		if (!$swiperEls.length) return;
310
311		var $btn = $("#personaBtn");
312		var $dropdown = $("#personaDropdown");
313		var $btnText = $("#personaBtnText");
314
315		if (!$btn.length || !$dropdown.length || !$btnText.length) return;
316
317		var swiperConfig = {
318			slidesPerView: 1.2,
319			centeredSlides: true,
320			loop: true,
321			spaceBetween: 0,
322			grabCursor: true,
323			scrollbar: {
324				el: ".programs-swiper__scrollbar",
325				draggable: true,
326			},
327			breakpoints: {
328				769: {
329					slidesPerView: "auto",
330					centeredSlides: 	false,
331					loop: false,
332				},
333			},
334		};
335
336		var swiperInstances = {};
337
338		function initSwiper($el) {
339			var idx = $el.index();
340			if (swiperInstances[idx]) {
341				swiperInstances[idx].destroy(true, true);
342			}
343			var config = $.extend({}, swiperConfig, {
344				scrollbar: { el: $el.find(".programs-swiper__scrollbar")[0], draggable: true },
345			});
346			var swiper = new Swiper($el[0], config);
347			swiperClickToSlide(swiper);
348			swiperInstances[idx] = swiper;
349		}
350
351		$swiperEls.each(function () {
352			initSwiper($(this));
353		});
354
355		function showPersona(value) {
356			$swiperEls.each(function () {
357				var $el = $(this);
358				var show = $el.data("persona") === value;
359				if ($el.is(":visible") === show) return;
360				if (show) {
361					$el.show();
362					initSwiper($el);
363				} else {
364					$el.hide();
365				}
366			});
367		}
368
369		function open() {
370			$dropdown.prop("hidden", false);
371			$btn.attr("aria-expanded", "true");
372		}
373
374		function close() {
375			$dropdown.prop("hidden", true);
376			$btn.attr("aria-expanded", "false");
377		}
378
379		$btn.on("click", function () {
380			if ($dropdown.prop("hidden")) {
381				open();
382			} else {
383				close();
384			}
385		});
386
387		$dropdown
388			.on("click", ".programs__persona-option", function () {
389				var $option = $(this);
390
391				$btnText.text($option.text().trim().toUpperCase());
392
393				$dropdown
394					.find(".programs__persona-option")
395					.attr("aria-selected", "false");
396
397				$option.attr("aria-selected", "true");
398
399				showPersona($option.data("value"));
400				close();
401
402				$btn.trigger("focus");
403			})
404			.on("keydown", function (e) {
405				var $opts = $dropdown.find(".programs__persona-option");
406				var idx = $opts.index(document.activeElement);
407
408				if (e.key === "ArrowDown") {
409					e.preventDefault();
410					$opts
411						.eq(Math.min(idx + 1, $opts.length - 1))
412						.trigger("focus");
413				} else if (e.key === "ArrowUp") {
414					e.preventDefault();
415					$opts.eq(Math.max(idx - 1, 0)).trigger("focus");
416				} else if (e.key === "Escape") {
417					close();
418					$btn.trigger("focus");
419				} else if (e.key === "Enter" || e.key === " ") {
420					e.preventDefault();
421					$(document.activeElement).trigger("click");
422				}
423			});
424
425		$(document).on("click", function (e) {
426			if (
427				!$btn.is(e.target) &&
428				!$btn.has(e.target).length &&
429				!$dropdown.is(e.target) &&
430				!$dropdown.has(e.target).length
431			) {
432				close();
433			}
434		});
435	}
436
437	/* ============================================================
438	   PROGRAMS SLIDER + POPUP
439	   ============================================================ */
440	function initProgramsSliderPopup() {
441		var $section = $(".programs-slider");
442		if (!$section.length) return;
443
444		/* Init Swiper */
445		var $swiperEl = $section.find("#programsSliderSwiper");
446		if ($swiperEl.length) {
447			new Swiper("#programsSliderSwiper", {
448				slidesPerView: 1.2,
449				centeredSlides: true,
450				loop: true,
451				spaceBetween: 0,
452				grabCursor: true,
453				scrollbar: {
454					el: $swiperEl.find(".programs-swiper__scrollbar")[0],
455					draggable: true,
456				},
457				breakpoints: {
458					769: {
459						slidesPerView: "auto",
460						centeredSlides: false,
461						loop: true,
462					},
463				},
464			});
465		}
466	}
467
468	/* ============================================================
469		   MISSION WORD REVEAL
470		   ============================================================ */
471	function initMissionReveal() {
472		var $targets = $(
473			".js-mission-reveal, [id='missionText'], [id='campsText']",
474		);
475		if (!$targets.length) return;
476
477		var revealers = [];
478
479		$targets.each(function () {
480			var $el = $(this);
481			if ($el.data("missionRevealReady")) {
482				return;
483			}
484
485			var plainText = $el.text().trim();
486			if (!plainText.length) return;
487
488			$el.html(
489				plainText
490					.split(/\s+/)
491					.map(function (w) {
492						return '<span class="mission__word">' + w + "</span>";
493					})
494					.join(" "),
495			);
496
497			var $spans = $el.find(".mission__word");
498			var $section = $el.closest(".mission").length
499				? $el.closest(".mission")
500				: $el.parent();
501			$el.data("missionRevealReady", true);
502
503			revealers.push(function () {
504				var rect = $section[0].getBoundingClientRect();
505				var progress = Math.min(
506					1,
507					Math.max(
508						0,
509						(window.innerHeight / 2 - rect.top) /
510							(rect.height * 0.7),
511					),
512				);
513				var count = Math.round(progress * $spans.length);
514
515				$spans.each(function (i) {
516					$(this).toggleClass("mission__word--revealed", i < count);
517				});
518			});
519		});
520
521		if (!revealers.length) return;
522
523		function revealAll() {
524			revealers.forEach(function (fn) {
525				fn();
526			});
527		}
528
529		$(window)
530			.off("scroll.missionReveal")
531			.on("scroll.missionReveal", revealAll);
532		revealAll();
533	}
534
535	/* ============================================================
536	   SCHEDULE TABS
537	   ============================================================ */
538	function initScheduleTabs() {
539		$(document).on("click", ".schedule__tab", function () {
540			var $tab = $(this);
541			var panelId = $tab.attr("aria-controls");
542
543			$(".schedule__tab").each(function () {
544				var $t = $(this);
545				$t.removeClass("schedule__tab--active").attr(
546					"aria-selected",
547					"false",
548				);
549				$("#" + $t.attr("aria-controls")).addClass(
550					"schedule__panel--hidden",
551				);
552			});
553
554			$tab.addClass("schedule__tab--active").attr(
555				"aria-selected",
556				"true",
557			);
558			$("#" + panelId).removeClass("schedule__panel--hidden");
559		});
560	}
561
562	/* ============================================================
563	   FAQ ACCORDION + LOAD MORE
564	   ============================================================ */
565	function initFaqAccordion() {
566		$(document).on('click', '.faq__question', function () {
567			var $question = $(this)
568			var $item = $question.closest('.faq__item')
569			var $section = $item.closest('.faq')
570			var isOpen = $item.hasClass('faq__item--open')
571
572			$section
573				.find('.faq__item--open')
574				.removeClass('faq__item--open')
575				.find('.faq__question')
576				.attr('aria-expanded', 'false')
577
578			if (!isOpen) {
579				$item.addClass('faq__item--open')
580				$question.attr('aria-expanded', 'true')
581			}
582		})
583
584		$(document).on("click", ".faq__load-more-btn", function () {
585			var $btn = $(this);
586			var $section = $btn.closest(".faq");
587			var $grid = $section.find(".faq__grid");
588			var $cols = $grid.find(".faq__col");
589			var perPage = $section.data("faq-per-page") || 5;
590
591			var remaining = $section.data("faq-remaining") || [];
592			if (!remaining.length) {
593				$btn.closest(".faq__load-more").remove();
594				return;
595			}
596
597			var itemsToLoad = remaining.slice(0, perPage);
598			var remainingItems = remaining.slice(perPage);
599
600			var $template = $section.find(".faq__item-template");
601			var templateHtml = $template.length
602				? $template.html()
603				: '<div class="faq__item"><button class="faq__question" aria-expanded="false"><span>{{question}}</span></button><div class="faq__answer">{{answer}}</div></div>';
604
605			var colIdx = 0;
606			itemsToLoad.forEach(function (item, i) {
607				var html = templateHtml
608					.replace(/{{question}}/g, item.question)
609					.replace(/{{answer}}/g, item.answer);
610				var $col = $cols.eq(colIdx);
611				$col.append(html);
612				if ((i + 1) % Math.ceil(itemsToLoad.length / 2) === 0) {
613					colIdx = (colIdx + 1) % 2;
614				}
615			});
616
617			$section.data("faq-remaining", remainingItems);
618
619			if (!remainingItems.length) {
620				$btn.closest(".faq__load-more").remove();
621			}
622		});
623	}
624
625	/* ============================================================
626	   CAMPS FILTER DROPDOWNS
627	   ============================================================ */
628	function initCampsFilters() {
629		if (!$(".camp-filter").length) return;
630
631		$(document)
632			.on("click", ".camps-catalogue__filter-btn", function (e) {
633				e.stopPropagation();
634
635				var $btn = $(this);
636				var $filter = $btn.closest(".camp-filter");
637				var $dropdown = $filter.find(".camp-filter__dropdown");
638				var isHidden = $dropdown.prop("hidden");
639
640				$(".camp-filter__dropdown").prop("hidden", true);
641				$(".camps-catalogue__filter-btn").attr(
642					"aria-expanded",
643					"false",
644				);
645
646				$dropdown.prop("hidden", !isHidden);
647				$btn.attr("aria-expanded", String(isHidden));
648			})
649
650			.on("click", ".camp-filter__dropdown", function (e) {
651				e.stopPropagation();
652
653				var $dropdown = $(this);
654				var $option = $(e.target).closest(".camp-filter__option");
655
656				if (!$option.length) return;
657
658				var $filter = $dropdown.closest(".camp-filter");
659				var isCheckbox = $filter.hasClass("camp-filter--checkbox");
660
661				if (isCheckbox) {
662					var isSelected = $option.attr("aria-selected") === "true";
663					$option.attr("aria-selected", String(!isSelected));
664				} else {
665					$dropdown
666						.find(".camp-filter__option")
667						.attr("aria-selected", "false");
668					$option.attr("aria-selected", "true");
669					$filter
670						.find(".camp-filter__label")
671						.text($option.text().trim());
672					$dropdown.prop("hidden", true);
673					$filter
674						.find(".camps-catalogue__filter-btn")
675						.attr("aria-expanded", "false");
676				}
677			})
678
679			.on("keydown", ".camp-filter__dropdown", function (e) {
680				var $dropdown = $(this);
681				var $opts = $dropdown.find(".camp-filter__option");
682				var idx = $opts.index(document.activeElement);
683				var $btn = $dropdown
684					.closest(".camp-filter")
685					.find(".camps-catalogue__filter-btn");
686
687				if (e.key === "ArrowDown") {
688					e.preventDefault();
689					$opts
690						.eq(Math.min(idx + 1, $opts.length - 1))
691						.trigger("focus");
692				} else if (e.key === "ArrowUp") {
693					e.preventDefault();
694					$opts.eq(Math.max(idx - 1, 0)).trigger("focus");
695				} else if (e.key === "Escape") {
696					$dropdown.prop("hidden", true);
697					$btn.attr("aria-expanded", "false").trigger("focus");
698				} else if (e.key === "Enter" || e.key === " ") {
699					e.preventDefault();
700					$(document.activeElement).trigger("click");
701				}
702			})
703
704			.on("click", function (e) {
705				if (!$(e.target).closest(".camp-filter").length) {
706					$(".camp-filter__dropdown").prop("hidden", true);
707					$(".camps-catalogue__filter-btn").attr(
708						"aria-expanded",
709						"false",
710					);
711				}
712			});
713	}
714
715	/* ============================================================
716	   GENERIC SCROLL SLIDER
717	   ============================================================ */
718	function initSlider(trackId, dotsSelector) {
719		var $track = $("#" + trackId);
720		var $dots = $(dotsSelector);
721		if (!$track.length || !$dots.length) return;
722
723		function updateDots(index) {
724			$dots.each(function (i) {
725				$(this)
726					.toggleClass("slider-progress__dot--active", i === index)
727					.attr("aria-selected", String(i === index));
728			});
729		}
730
731		$dots.on("click", function () {
732			var index = parseInt($(this).data("index"), 10);
733			var $items = $track.children();
734			if ($items.eq(index).length) {
735				$track[0].scrollTo({
736					left: $items[index].offsetLeft - $track[0].offsetLeft,
737					behavior: "smooth",
738				});
739			}
740			updateDots(index);
741		});
742
743		var observer = new IntersectionObserver(
744			function (entries) {
745				entries.forEach(function (entry) {
746					if (!entry.isIntersecting) return;
747					var index = Array.from($track[0].children).indexOf(
748						entry.target,
749					);
750					if (index !== -1) updateDots(index);
751				});
752			},
753			{ root: $track[0], threshold: 0.5 },
754		);
755
756		$track.children().each(function () {
757			observer.observe(this);
758		});
759	}
760
761	/* ============================================================
762	   FEATURED TABS
763	   ============================================================ */
764	function initFeaturedTabs() {
765		$(document).on("click", ".featured__tab", function () {
766			var panelId = $(this).attr("aria-controls");
767			$(".featured__tab")
768				.removeClass("featured__tab--active")
769				.attr("aria-selected", "false");
770			$(".featured__panel").removeClass("featured__panel--active");
771			$(this)
772				.addClass("featured__tab--active")
773				.attr("aria-selected", "true");
774			$("#" + panelId).addClass("featured__panel--active");
775		});
776	}
777
778	/* ============================================================
779	   INVOLVED TABS
780	   ============================================================ */
781	function initInvolvedTabs() {
782		$(".involved__tabs").each(function () {
783			var $tabGroup = $(this);
784			var $tabs = $tabGroup.find(".involved__tab");
785			if (!$tabs.length) return;
786
787			function activate(panelId) {
788				$tabs.each(function () {
789					$(this)
790						.removeClass("involved__tab--active")
791						.attr("aria-selected", "false");
792					$("#" + $(this).attr("aria-controls")).addClass(
793						"involved__panel--hidden",
794					);
795				});
796				$tabGroup
797					.find('[aria-controls="' + panelId + '"]')
798					.addClass("involved__tab--active")
799					.attr("aria-selected", "true");
800				$("#" + panelId).removeClass("involved__panel--hidden");
801			}
802
803			$tabs.on("click", function () {
804				activate($(this).attr("aria-controls"));
805			});
806
807			var $select = $('<select class="involved__tabs-select">').attr(
808				"aria-label",
809				$tabGroup.attr("aria-label") || "Select tab",
810			);
811			$tabs.each(function () {
812				$("<option>")
813					.val($(this).attr("aria-controls"))
814					.text($(this).text().trim())
815					.prop("selected", $(this).attr("aria-selected") === "true")
816					.appendTo($select);
817			});
818			$select.on("change", function () {
819				activate($(this).val());
820			});
821			$tabGroup.after($select);
822		});
823	}
824
825	/* ============================================================
826	   TEAM CARD EXPAND
827	   ============================================================ */
828	function initTeamCardExpand() {
829		$(document)
830			.on("click", ".team-card__expand", function () {
831				$(this).closest(".team-card").addClass("team-card--expanded");
832				$(this).attr("aria-expanded", "true");
833			})
834			.on("click", ".team-card__collapse", function () {
835				var $card = $(this).closest(".team-card");
836				$card.removeClass("team-card--expanded");
837				$card.find(".team-card__expand").attr("aria-expanded", "false");
838			});
839	}
840
841	/* ============================================================
842	   NOTIFY ME — Klaviyo onsite popup with program context
843	   ============================================================ */
844	function initNotifyMe() {
845		$(document).on('click', '.js-klaviyo-notify', function () {
846			var $btn = $(this);
847			var formId = $btn.data('form') || (window.spyhopNotify && window.spyhopNotify.formId) || 'TKDYXx';
848			var props = {
849				'Program Interest': $btn.data('program-title') || document.title,
850				'Program URL':      $btn.data('program-url') || window.location.href,
851				'Program ID':       String($btn.data('program-id') || ''),
852				'Program Type':     $btn.data('program-type') || ''
853			};
854			// Pass context to Klaviyo so form submission is attributed to this program.
855			// Requires hidden fields with same names in Klaviyo form builder (optional).
856			try {
857				if (window.klaviyo) {
858					window.klaviyo.push(['identify', props]);
859				} else if (window._learnq) {
860					window._learnq.push(['identify', props]);
861				}
862			} catch (e) {}
863			window._klOnsite = window._klOnsite || [];
864			window._klOnsite.push(['openForm', formId]);
865		});
866	}
867
868	/* ============================================================
869	   NEWSLETTER FORM
870	   ============================================================ */
871	function initNewsletterForm() {
872		var $form = $(".site-footer__form"),
873			$input = $form.find(".site-footer__input"),
874			$row = $form.find(".site-footer__form-row"),
875			$submit = $form.find(".site-footer__submit"),
876			$submitSvg = $submit.find("svg");
877
878		if (!$form.length) return;
879
880		$form.on("submit", function (e) {
881			e.preventDefault();
882
883			var email = $input.val().trim();
884			if (!email) return;
885
886			if (typeof validate !== "undefined") {
887				var v = validate({ email: email }, { email: { email: { message: "Enter a valid email" } } });
888				if (v) {
889					$row.addClass("site-footer__form-row--error");
890					$input.one("input", function () { $row.removeClass("site-footer__form-row--error"); });
891					return;
892				}
893			}
894
895			$row.removeClass("site-footer__form-row--error");
896			$submit.prop("disabled", true).addClass("site-footer__submit--loading");
897			$submitSvg.hide();
898
899			$.post(spyhopNewsletter.ajax_url, {
900				action: "spyhop_newsletter_subscribe",
901				nonce: spyhopNewsletter.nonce,
902				email: email,
903			})
904				.done(function (res) {
905					if (res.success) {
906						showToast(res.data.message || "Thank you for subscribing!", "success");
907						$input.val("");
908					} else {
909						showToast(res.data.message || "Something went wrong.", "error");
910					}
911				})
912				.fail(function () {
913					showToast("Network error. Please try again.", "error");
914				})
915				.always(function () {
916					$submit.prop("disabled", false).removeClass("site-footer__submit--loading");
917					$submitSvg.show();
918				});
919		});
920	}
921
922	/* ============================================================
923	   TOAST NOTIFICATION
924	   ============================================================ */
925	function showToast(message, type) {
926		var $toast = $(
927			'<div class="toast toast--' + type + '">' +
928				'<span class="toast__message">' + message + "</span>" +
929				'<button class="toast__close" aria-label="Close">&times;</button>' +
930			"</div>"
931		);
932
933		$toast.find(".toast__close").on("click", function () {
934			removeToast($toast);
935		});
936
937		setTimeout(function () {
938			removeToast($toast);
939		}, 5000);
940
941		$("body").append($toast);
942		requestAnimationFrame(function () {
943			$toast.addClass("toast--visible");
944		});
945	}
946
947	function removeToast($toast) {
948		if ($toast.hasClass("toast--removing")) return;
949		$toast.addClass("toast--removing");
950		$toast.on("transitionend", function () {
951			$toast.remove();
952		});
953	}
954
955	window.showToast = showToast;
956
957	/* ============================================================
958	   MEGA MENU CTA OVERLAY
959	   ============================================================ */
960	function initMegaMenuCtaOverlay() {
961		$(document)
962			.on("click", ".mega-menu__cta-btn", function (e) {
963				e.preventDefault();
964				e.stopPropagation();
965				$(this)
966					.closest(".mega-menu__cta")
967					.find(".mega-menu__cta-overlay")
968					.addClass("mega-menu__cta-overlay--open")
969					.attr("aria-hidden", "false");
970			})
971			.on("click", ".mega-menu__cta-overlay-close", function (e) {
972				e.stopPropagation();
973				$(this)
974					.closest(".mega-menu__cta-overlay")
975					.removeClass("mega-menu__cta-overlay--open")
976					.attr("aria-hidden", "true");
977			})
978			.on("mouseleave", ".nav-item--has-mega", function () {
979				$(this)
980					.find(".mega-menu__cta-overlay")
981					.removeClass("mega-menu__cta-overlay--open")
982					.attr("aria-hidden", "true");
983			});
984	}
985
986	/* ============================================================
987	   CAMPS FILTER DRAWER (mobile)
988	   ============================================================ */
989	function initCampsFilterDrawer() {
990		var $drawer = $("#campsFilterDrawer");
991		if (!$drawer.length) return;
992
993		function close() {
994			$drawer
995				.removeClass("camps-filter-drawer--open")
996				.attr("aria-hidden", "true");
997			$("body").removeClass("scroll-locked");
998		}
999
1000		$(document).on("click", "#campsMobileFilterBtn", function () {
1001			$drawer
1002				.addClass("camps-filter-drawer--open")
1003				.attr("aria-hidden", "false");
1004			$("body").addClass("scroll-locked");
1005		});
1006
1007		$(document).on("click", "#campsFilterClose, #campsFilterOverlay", close);
1008
1009		$(document).on("keydown", function (e) {
1010			if (e.key === "Escape" && $drawer.hasClass("camps-filter-drawer--open")) {
1011				close();
1012			}
1013		});
1014
1015		$drawer.on("click", ".camps-filter-drawer__section-btn", function () {
1016			var $section = $(this).closest(".camps-filter-drawer__section");
1017			var isOpen = $section.hasClass("camps-filter-drawer__section--open");
1018			$section.toggleClass("camps-filter-drawer__section--open", !isOpen);
1019			$(this).attr("aria-expanded", String(!isOpen));
1020		});
1021	}
1022
1023	/* ============================================================
1024	   GALLERY MOSAIC SLIDER + FANCYBOX
1025	   ============================================================ */
1026	function initGalleryMosaic() {
1027		$(".gallery-mosaic__desktop-swiper").each(function () {
1028			if (!this.id) return;
1029			var swiper = new Swiper("#" + this.id, {
1030				slidesPerView: "auto",
1031				freeMode: true,
1032				grabCursor: true,
1033			});
1034			swiperClickToSlide(swiper);
1035		});
1036
1037		$(".gallery-mosaic__mobile-swiper, .post-mosaic__swiper").each(function () {
1038			if (!this.id) return;
1039			var swiper = new Swiper("#" + this.id, {
1040				slidesPerView: 1,
1041				grabCursor: true,
1042			});
1043			swiperClickToSlide(swiper);
1044		});
1045
1046		if (typeof Fancybox !== "undefined") {
1047			Fancybox.bind("[data-fancybox]", { groupAll: false });
1048		}
1049	}
1050
1051	/* ============================================================
1052	   EXAMPLE EVENTS REVEAL
1053	   ============================================================ */
1054	function initExampleEventsReveal() {
1055		var sections = document.querySelectorAll(".js-example-events");
1056		if (!sections.length) return;
1057
1058		if (!("IntersectionObserver" in window)) {
1059			sections.forEach(function (section) {
1060				section.classList.add("is-visible");
1061			});
1062			return;
1063		}
1064
1065		var observer = new IntersectionObserver(
1066			function (entries, obs) {
1067				entries.forEach(function (entry) {
1068					if (!entry.isIntersecting) return;
1069					entry.target.classList.add("is-visible");
1070					obs.unobserve(entry.target);
1071				});
1072			},
1073			{ threshold: 0.2 },
1074		);
1075
1076		sections.forEach(function (section) {
1077			observer.observe(section);
1078		});
1079	}
1080
1081	/* ============================================================
1082		   PRICING PACKAGES REVEAL
1083		   ============================================================ */
1084	function initPricingPackagesReveal() {
1085		var sections = document.querySelectorAll(".js-pricing-packages");
1086		if (!sections.length) return;
1087
1088		if (!("IntersectionObserver" in window)) {
1089			sections.forEach(function (section) {
1090				section.classList.add("is-visible");
1091			});
1092			return;
1093		}
1094
1095		var observer = new IntersectionObserver(
1096			function (entries, obs) {
1097				entries.forEach(function (entry) {
1098					if (!entry.isIntersecting) return;
1099					entry.target.classList.add("is-visible");
1100					obs.unobserve(entry.target);
1101				});
1102			},
1103			{ threshold: 0.18 },
1104		);
1105
1106		sections.forEach(function (section) {
1107			observer.observe(section);
1108		});
1109	}
1110
1111	/* ============================================================
1112	   RESOURCES TABS
1113	   ============================================================ */
1114	function initResourcesTabs() {
1115		$(document).on("click", ".resources__tab", function () {
1116			var panelId = $(this).attr("aria-controls");
1117			$(".resources__tab").each(function () {
1118				$(this)
1119					.removeClass("resources__tab--active")
1120					.attr("aria-selected", "false");
1121				$("#" + $(this).attr("aria-controls")).addClass(
1122					"resources__panel--hidden",
1123				);
1124			});
1125			$(this)
1126				.addClass("resources__tab--active")
1127				.attr("aria-selected", "true");
1128			$("#" + panelId).removeClass("resources__panel--hidden");
1129		});
1130	}
1131
1132	/* ============================================================
1133	   CAMPS CATALOGUE FILTER (AJAX)
1134	   ============================================================ */
1135	function initCampsFilter() {
1136		console.log("[camps] initCampsFilter");
1137		var $grid = $("#campsCatalogueGrid");
1138		var $section = $("#camps-catalogue");
1139		if (!$grid.length || !$section.length) { console.log("[camps] skip: no grid/section"); return; }
1140
1141		var $loadMoreBtn = $("#campsLoadMore");
1142		var $countEl = $(".camps-catalogue__count");
1143		var $resetBtns = $(".camps-catalogue__reset");
1144
1145		var postIds = String($section.data("post-ids") || "")
1146			.split(",")
1147			.map(Number)
1148			.filter(Boolean);
1149		var perPage = parseInt($section.data("per-page"), 10) || 9;
1150		var programType = String($section.data("program-type") || "");
1151		var initialCards = $grid.find(".camp-card").length;
1152		console.log("[camps] init programType=" + programType + " data-attr=" + $section.attr("data-program-type") + " initialCards=" + initialCards + " perPage=" + perPage);
1153		var labelTaxes = String($section.data("label-taxes") || "");
1154		console.log("[camps] labelTaxes=" + labelTaxes);
1155
1156		var activeFilters = {};
1157		var currentPage = 1;
1158		var isLoading = false;
1159		var mobileLimited = false;
1160		var storageKey = "campsCatalogue_" + programType;
1161
1162		function saveCampsState() {
1163			try {
1164				var state = { currentPage: currentPage, scrollY: window.scrollY };
1165				sessionStorage.setItem(storageKey, JSON.stringify(state));
1166			} catch (e) {}
1167		}
1168
1169		function clearCampsState() {
1170			try { sessionStorage.removeItem(storageKey); } catch (e) {}
1171		}
1172
1173		/* ── Mobile: limit initial view to 5 cards ── */
1174		function applyMobileLimit() {
1175			if ($(window).width() > 768) { mobileLimited = false; return; }
1176			var $cards = $grid.find(".camp-card");
1177			if ($cards.length > 5) {
1178				$cards.slice(5).hide();
1179				mobileLimited = true;
1180			} else {
1181				mobileLimited = false;
1182			}
1183		}
1184		applyMobileLimit();
1185
1186		/* ── Build AJAX payload ── */
1187		function buildPayload(page) {
1188			var payload = {
1189				action: "spyhop_catalogue",
1190				nonce: spyhopAjax.nonce,
1191				paged: page,
1192				per_page: perPage,
1193				program_type: programType,
1194				label_taxes: labelTaxes,
1195				filters: {},
1196			};
1197			if (postIds.length) payload.post__in = postIds;
1198
1199			$.each(activeFilters, function (source, values) {
1200				if (values === null || values === undefined) return;
1201				var arr = Array.isArray(values) ? values : [values];
1202				arr = $.grep(arr, function (v) {
1203					return v !== "";
1204				});
1205				if (arr.length) payload.filters[source] = arr;
1206			});
1207
1208			return payload;
1209		}
1210
1211		/* ── Staggered entrance animation for new cards ── */
1212		function animateIn($cards) {
1213			$cards.addClass("camp-card--new");
1214			$cards.each(function (i) {
1215				$(this).css("animation-delay", i * 0.045 + "s");
1216			});
1217			setTimeout(function () {
1218				$cards.removeClass("camp-card--new").css("animation-delay", "");
1219			}, 900);
1220		}
1221
1222		/* ── Core fetch ── */
1223		function doFetch(page, append) {
1224			if (isLoading) return;
1225			isLoading = true;
1226			if (!append) $grid.addClass("is-fetching");
1227
1228			$.ajax({
1229				url: spyhopAjax.url,
1230				type: "POST",
1231				data: buildPayload(page),
1232				success: function (res) {
1233					$grid.removeClass("is-fetching");
1234					if (!res.success) {
1235						console.log("[camps] doFetch failed: not success");
1236						isLoading = false;
1237						return;
1238					}
1239
1240					var d = res.data;
1241					var html = d.html || "";
1242					console.log("[camps] doFetch ok: has_more=" + d.has_more + " total=" + d.total + " paged=" + d.paged + " htmlLen=" + html.length);
1243					currentPage = d.paged;
1244
1245					if (append) {
1246						var $new = $($.parseHTML(html));
1247						$grid.append($new);
1248						animateIn($new.filter(".camp-card"));
1249						saveCampsState();
1250					} else {
1251						var $new = $($.parseHTML(html));
1252						animateIn($new.filter(".camp-card"));
1253						$grid.empty().append($new);
1254						applyMobileLimit();
1255						clearCampsState();
1256					}
1257					if (d.has_more) {
1258						$loadMoreBtn.removeAttr("hidden");
1259					} else {
1260						$loadMoreBtn.attr("hidden", "");
1261					}
1262					if ($countEl.length) {
1263						$countEl.text(
1264							d.total > 0 ? d.total + " PROGRAMS" : "0 PROGRAMS",
1265						);
1266					}
1267					isLoading = false;
1268				},
1269				error: function (jqXHR, textStatus) {
1270					console.log("[camps] doFetch error:", textStatus);
1271					$grid.removeClass("is-fetching");
1272					isLoading = false;
1273				},
1274			});
1275		}
1276
1277		/* ── Sync drawer state from activeFilters ── */
1278		function syncDrawer(source) {
1279			var arr = activeFilters[source]
1280				? Array.isArray(activeFilters[source])
1281					? activeFilters[source]
1282					: [String(activeFilters[source])]
1283				: [];
1284			$(
1285				'.camps-filter-drawer__section[data-filter="' +
1286					source +
1287					'"] .camps-filter-drawer__option',
1288			).each(function () {
1289				var v = String($(this).data("value") || "");
1290				$(this).attr(
1291					"aria-selected",
1292					v && arr.indexOf(v) !== -1 ? "true" : "false",
1293				);
1294			});
1295		}
1296
1297		/* ── Sync desktop filter state from activeFilters ── */
1298		function syncDesktop(source) {
1299			var arr = activeFilters[source]
1300				? Array.isArray(activeFilters[source])
1301					? activeFilters[source]
1302					: [String(activeFilters[source])]
1303				: [];
1304			var $filter = $('.camp-filter[data-filter="' + source + '"]');
1305			var isCheckbox = $filter.hasClass("camp-filter--checkbox");
1306
1307			$filter.find(".camp-filter__option").each(function () {
1308				var v = String($(this).data("value") || "");
1309				var selected = v ? arr.indexOf(v) !== -1 : arr.length === 0;
1310				$(this).attr("aria-selected", selected ? "true" : "false");
1311			});
1312
1313			if (!isCheckbox) {
1314				var activeText = $filter
1315					.find('.camp-filter__option[aria-selected="true"]')
1316					.first()
1317					.text()
1318					.trim();
1319				$filter.find(".camp-filter__label").text(activeText);
1320			}
1321		}
1322
1323		/* ── Desktop filter option click ── */
1324		$(document).on("click", ".camp-filter__option", function () {
1325			var $filter = $(this).closest(".camp-filter");
1326			var source = String($filter.data("filter"));
1327			var value = String($(this).data("value") || "");
1328			var isCheckbox = $filter.hasClass("camp-filter--checkbox");
1329
1330			if (isCheckbox) {
1331				var current = Array.isArray(activeFilters[source])
1332					? activeFilters[source].slice()
1333					: [];
1334				var idx = current.indexOf(value);
1335				if (value) {
1336					if (idx === -1) current.push(value);
1337					else current.splice(idx, 1);
1338				}
1339				activeFilters[source] = current.length ? current : null;
1340			} else {
1341				activeFilters[source] = value || null;
1342			}
1343
1344			syncDrawer(source);
1345			currentPage = 1;
1346			doFetch(1, false);
1347		});
1348
1349		/* ── Drawer option click ── */
1350		$(document).on("click", ".camps-filter-drawer__option", function () {
1351			var $sec = $(this).closest(".camps-filter-drawer__section");
1352			var source = String($sec.data("filter"));
1353			var isSelected = $(this).attr("aria-selected") === "true";
1354			$(this).attr("aria-selected", isSelected ? "false" : "true");
1355
1356			var selected = [];
1357			$sec.find(
1358				'.camps-filter-drawer__option[aria-selected="true"]',
1359			).each(function () {
1360				var v = String($(this).data("value") || "");
1361				if (v) selected.push(v);
1362			});
1363			activeFilters[source] = selected.length ? selected : null;
1364
1365			syncDesktop(source);
1366			currentPage = 1;
1367			doFetch(1, false);
1368
1369			$("#campsFilterClose").trigger("click");
1370		});
1371
1372		/* ── Reset ── */
1373		$resetBtns.on("click", function () {
1374			activeFilters = {};
1375			$(".camp-filter__option").attr("aria-selected", "false");
1376			$(
1377				".camp-filter:not(.camp-filter--checkbox) .camp-filter__option--all",
1378			)
1379				.attr("aria-selected", "true")
1380				.each(function () {
1381					$(this)
1382						.closest(".camp-filter")
1383						.find(".camp-filter__label")
1384						.text(
1385							$(this)
1386								.closest(".camp-filter")
1387								.data("default-label") || "",
1388						);
1389				});
1390			$(".camps-filter-drawer__option").attr("aria-selected", "false");
1391			currentPage = 1;
1392			doFetch(1, false);
1393		});
1394
1395		/* ── Save scroll when clicking a camp card ── */
1396		$(document).on("click", ".camp-card a", function () {
1397			if (currentPage > 1) saveCampsState();
1398		});
1399
1400		/* ── Load More ── */
1401		$loadMoreBtn.on("click", function () {
1402			console.log("[camps] load more click: mobileLimited=" + mobileLimited + " currentPage=" + currentPage);
1403			// Mobile: 5 cards initially — first click reveals remaining local cards
1404			if (mobileLimited) {
1405				$grid.find(".camp-card").show();
1406				mobileLimited = false;
1407				var hasMore = $section.data("has-more") ? true : false;
1408				if (!hasMore) $loadMoreBtn.attr("hidden", "");
1409				return;
1410			}
1411			doFetch(currentPage + 1, true);
1412		});
1413
1414		/* ── Restore state on back navigation ── */
1415		(function () {
1416			var saved;
1417			try { saved = JSON.parse(sessionStorage.getItem(storageKey)); } catch (e) {}
1418			console.log("[camps] saved state:", saved);
1419			if (saved && saved.currentPage > 1) {
1420				console.log("[camps] restoring to page " + saved.currentPage);
1421				var targetPage = saved.currentPage;
1422				var restoreScroll = saved.scrollY || 0;
1423				$loadMoreBtn.attr("hidden", "");
1424				(function fetchChain(page) {
1425					if (page > targetPage) {
1426						if (restoreScroll) { $("html, body").animate({ scrollTop: restoreScroll }, 400); }
1427						return;
1428					}
1429					$.ajax({
1430						url: spyhopAjax.url,
1431						type: "POST",
1432						data: buildPayload(page),
1433						success: function (res) {
1434							if (res.success && res.data.html) {
1435								var d = res.data;
1436								var h = d.html || "";
1437								currentPage = d.paged;
1438								var $new = $($.parseHTML(h));
1439								$grid.append($new);
1440								animateIn($new.filter(".camp-card"));
1441								if (d.has_more) {
1442									$loadMoreBtn.removeAttr("hidden");
1443								} else {
1444									$loadMoreBtn.attr("hidden", "");
1445								}
1446								if ($countEl.length) {
1447									$countEl.text(
1448										d.total > 0 ? d.total + " PROGRAMS" : "0 PROGRAMS",
1449									);
1450								}
1451							}
1452							fetchChain(page + 1);
1453						},
1454					});
1455				})(2);
1456			}
1457		})();
1458	}
1459
1460	/* ============================================================
1461	   TOGGLE NAVIGATOR
1462	   ============================================================ */
1463	function initToggleNav() {
1464		$(document).on("click", ".toggle-nav__btn", function () {
1465			var panelId = $(this).attr("aria-controls");
1466			$(".toggle-nav__btn")
1467				.removeClass("toggle-nav__btn--active")
1468				.attr("aria-selected", "false")
1469				.each(function () {
1470					var p = $(this).attr("aria-controls");
1471					if (p) $("#" + p).prop("hidden", true);
1472				});
1473			$(this)
1474				.addClass("toggle-nav__btn--active")
1475				.attr("aria-selected", "true");
1476			$("#" + panelId).prop("hidden", false);
1477		});
1478	}
1479
1480	/* ============================================================
1481	   SPY HOP WAY — COLUMN EXPAND (click toggle)
1482	   ============================================================ */
1483	function initSpyHopWay() {
1484		function openCol($col) {
1485			$col.addClass("spy-hop-way__col__open");
1486			$col.find(".spy-hop-way__col_btn").attr("aria-expanded", "true");
1487		}
1488
1489		function closeCol($col) {
1490			$col.removeClass("spy-hop-way__col__open");
1491			setTimeout(function () {
1492				$col.find(".spy-hop-way__col_btn").attr("aria-expanded", "false");
1493			}, 300);
1494		}
1495
1496		$(document).on("click", ".spy-hop-way__col_btn", function () {
1497			var $col = $(this).closest(".spy-hop-way__col");
1498			var isOpen = $col.hasClass("spy-hop-way__col__open");
1499			if (isOpen) {
1500				closeCol($col);
1501			} else {
1502			$(".spy-hop-way__col").not($col).each(function () {
1503					closeCol($(this));
1504				});
1505				openCol($col);
1506			}
1507		});
1508
1509		$(document).on("click", ".spy-hop-way__col_close", function () {
1510			var $col = $(this).closest(".spy-hop-way__col");
1511			closeCol($col);
1512		});
1513	}
1514
1515	/* ============================================================
1516	   PROGRAMS FILTER (static)
1517	   ============================================================ */
1518	function initProgramsFilter() {
1519		var $grid = $("#programsCatalogueGrid");
1520		if (!$grid.length) return;
1521
1522		var activeFilters = {
1523			skill: null,
1524			medium: null,
1525			age: null,
1526			season: null,
1527		};
1528		var cycleMap = {
1529			skill: ["beginner", "intermediate", "advanced", null],
1530			medium: ["film", "music", "audio", "design", "mixed-media", null],
1531			age: ["youth", "teen", null],
1532			season: ["summer", "spring", null],
1533		};
1534
1535		function applyFilters() {
1536			$grid.find(".program-card").each(function () {
1537				var $card = $(this);
1538				var match =
1539					(!activeFilters.skill ||
1540						$card.data("skill") === activeFilters.skill) &&
1541					(!activeFilters.medium ||
1542						$card.data("medium") === activeFilters.medium) &&
1543					(!activeFilters.age ||
1544						$card.data("age") === activeFilters.age) &&
1545					(!activeFilters.season ||
1546						$card.data("season") === activeFilters.season);
1547				$card.toggle(match);
1548			});
1549		}
1550
1551		$("[data-filter]").on("click", function () {
1552			var key = $(this).data("filter");
1553			var values = cycleMap[key];
1554			if (!values) return;
1555			var idx = values.indexOf(activeFilters[key]);
1556			activeFilters[key] = values[(idx + 1) % values.length];
1557			applyFilters();
1558		});
1559
1560		$("#programsReset").on("click", function () {
1561			$.each(activeFilters, function (k) {
1562				activeFilters[k] = null;
1563			});
1564			$grid.find(".program-card").show();
1565		});
1566
1567		$("#programsLoadMore").on("click", function () {
1568			$(this).text("No more programs to show.").prop("disabled", true);
1569		});
1570	}
1571
1572	/* ============================================================
1573	   EVENTS FILTER (Featured Events/News Tabs)
1574	   ============================================================ */
1575	function initEventsFilter() {
1576		var $section = $("#featuredEventsNews");
1577		if (!$section.length) return;
1578
1579		var $grid = $("#upcomingEventsGrid");
1580		var $dropdown = $(".events-filter__dropdown");
1581		var $resetBtn = $(".events-filter__reset");
1582		var $label = $(".events-filter__label");
1583
1584		var isLoading = false;
1585		var activeType = "";
1586
1587		function doFetch(type) {
1588			if (isLoading) return;
1589			isLoading = true;
1590
1591			$.ajax({
1592				url: spyhopAjax.url,
1593				type: "POST",
1594				data: {
1595					action: "spyhop_events_filter",
1596					nonce: spyhopAjax.events_nonce,
1597					event_type: type,
1598				},
1599				success: function (res) {
1600					isLoading = false;
1601					if (!res.success) return;
1602
1603					$grid.html(res.data.html);
1604				},
1605			});
1606		}
1607
1608		$dropdown.on("click", ".events-filter__option", function () {
1609			var val = $(this).data("value") || "";
1610			activeType = activeType === val ? "" : val;
1611			$dropdown
1612				.find(".events-filter__option")
1613				.attr("aria-selected", "false");
1614			if (activeType) {
1615				$(this).attr("aria-selected", "true");
1616				$label.text($(this).text().toUpperCase());
1617				$resetBtn.show();
1618			} else {
1619				$label.text("TYPE");
1620				$resetBtn.hide();
1621			}
1622			doFetch(activeType);
1623		});
1624
1625		$resetBtn.on("click", function () {
1626			activeType = "";
1627			$label.text("TYPE");
1628			$resetBtn.hide();
1629			$dropdown
1630				.find(".events-filter__option")
1631				.attr("aria-selected", "false");
1632			doFetch("");
1633		});
1634
1635		/* Past Events LOAD MORE */
1636		var $loadMore = $("#pastEventsLoadMore");
1637		var $pastGrid = $("#pastEventsGrid");
1638		var pastPage = 1;
1639		var pastPerPage = parseInt($section.data("past-per-page"), 10) || 6;
1640		var totalPast = parseInt($section.data("total-past"), 10) || 0;
1641
1642		$loadMore.on("click", function () {
1643			if ($(this).prop("disabled")) return;
1644			pastPage++;
1645
1646			$.ajax({
1647				url: spyhopAjax.url,
1648				type: "POST",
1649				data: {
1650					action: "spyhop_past_events_load",
1651					nonce: spyhopAjax.events_nonce,
1652					paged: pastPage,
1653					per_page: pastPerPage,
1654				},
1655				success: function (res) {
1656					if (!res.success) return;
1657					$pastGrid.append(res.data.html);
1658					if (!res.data.has_more) {
1659						$loadMore
1660							.prop("disabled", true)
1661							.text("No more events.");
1662					}
1663				},
1664			});
1665		});
1666	}
1667
1668	/* ============================================================
1669	   NEWS FILTER + LOAD MORE (Featured News Tab)
1670	   ============================================================ */
1671	function initNewsFilter() {
1672		var $section = $("#featuredEventsNews");
1673		if (!$section.length) return;
1674
1675		var $grid = $("#newsGrid");
1676		var $loadMore = $("#newsLoadMore");
1677		var $resetBtn = $(".news-filter__reset");
1678		var $authorLabel = $(".news-filter__label--author");
1679		var $topicLabel = $(".news-filter__label--topic");
1680		var $authorOpts = $(
1681			".news-filter__dropdown--author .news-filter__option",
1682		);
1683		var $topicOpts = $(
1684			".news-filter__dropdown--topic .news-filter__option",
1685		);
1686
1687		if (!$grid.length) return;
1688
1689		var activeAuthor = "";
1690		var activeTopic = "";
1691		var newsPage = 1;
1692		var newsPerPage = parseInt($section.data("news-per-page"), 10) || 6;
1693		var isLoading = false;
1694		var isFetchedAll = false;
1695
1696		function applyFilters() {
1697			$grid.find(".camp-card").each(function () {
1698				var $card = $(this);
1699				var matchAuthor =
1700					!activeAuthor || $card.data("author") === activeAuthor;
1701				var matchTopic =
1702					!activeTopic || $card.data("topic") === activeTopic;
1703				$card.toggle(matchAuthor && matchTopic);
1704			});
1705		}
1706
1707		/* Author dropdown click */
1708		$(document).on(
1709			"click",
1710			".news-filter__dropdown--author .news-filter__option",
1711			function () {
1712				var val = $(this).data("value") || "";
1713				activeAuthor = activeAuthor === val ? "" : val;
1714				$authorOpts.attr("aria-selected", "false");
1715				if (activeAuthor) {
1716					$(this).attr("aria-selected", "true");
1717					$authorLabel.text(activeAuthor.toUpperCase());
1718				} else {
1719					$authorLabel.text("AUTHOR");
1720				}
1721				applyFilters();
1722			},
1723		);
1724
1725		/* Topic dropdown click */
1726		$(document).on(
1727			"click",
1728			".news-filter__dropdown--topic .news-filter__option",
1729			function () {
1730				var val = $(this).data("value") || "";
1731				activeTopic = activeTopic === val ? "" : val;
1732				$topicOpts.attr("aria-selected", "false");
1733				if (activeTopic) {
1734					$(this).attr("aria-selected", "true");
1735					$topicLabel.text($(this).text().toUpperCase());
1736				} else {
1737					$topicLabel.text("TOPIC");
1738				}
1739				applyFilters();
1740			},
1741		);
1742
1743		/* Reset */
1744		$resetBtn.on("click", function () {
1745			activeAuthor = "";
1746			activeTopic = "";
1747			$authorLabel.text("AUTHOR");
1748			$topicLabel.text("TOPIC");
1749			$authorOpts.attr("aria-selected", "false");
1750			$topicOpts.attr("aria-selected", "false");
1751			applyFilters();
1752		});
1753
1754		/* Load More */
1755		$loadMore.on("click", function () {
1756			if (isLoading || isFetchedAll) return;
1757			isLoading = true;
1758			newsPage++;
1759
1760			$.ajax({
1761				url: spyhopAjax.url,
1762				type: "POST",
1763				data: {
1764					action: "spyhop_featured_news_load",
1765					nonce: $section.data("featured-nonce") || spyhopAjax.featured_nonce,
1766					paged: newsPage,
1767					per_page: newsPerPage,
1768				},
1769				success: function (res) {
1770					isLoading = false;
1771					if (!res.success) return;
1772					var $new = $($.parseHTML(res.data.html));
1773					$grid.append($new);
1774					applyFilters();
1775					if (!res.data.has_more) {
1776						isFetchedAll = true;
1777						$loadMore.prop("disabled", true).text("NO MORE NEWS");
1778					}
1779				},
1780				error: function () {
1781					isLoading = false;
1782				},
1783			});
1784		});
1785	}
1786
1787	/* ============================================================
1788	   INIT
1789	   ============================================================ */
1790	$(function () {
1791		initStickyHeader();
1792		initLenis();
1793		initMegaMenu();
1794		initMobileMenu();
1795		initAnnouncementSwiper();
1796		initNewsSwiper();
1797		initEventsSwiper();
1798		initprogramssliderSwiper();
1799		initProgramsSwiper();
1800		initProgramsSliderPopup();
1801		initMissionReveal();
1802		initScheduleTabs();
1803		initFaqAccordion();
1804		initCampsFilters();
1805		initSlider(
1806			"relatedTrack",
1807			"#relatedTrack ~ .slider-progress .slider-progress__dot",
1808		);
1809		initFeaturedTabs();
1810		initInvolvedTabs();
1811		initCampsFilterDrawer();
1812		initTeamCardExpand();
1813		initNotifyMe();
1814		initNewsletterForm();
1815		initMegaMenuCtaOverlay();
1816		initGalleryMosaic();
1817		initExampleEventsReveal();
1818		initPricingPackagesReveal();
1819		initResourcesTabs();
1820		initCampsFilter();
1821		initToggleNav();
1822		initSpyHopWay();
1823		initProgramsFilter();
1824		initEventsFilter();
1825		initNewsFilter();
1826		initsingleeventsNewsSwiper();
1827		initByTheNumbers();
1828		initYouthMatters();
1829
1830		$(".site-footer__back-top").on("click", function (e) {
1831			e.preventDefault();
1832			$("html, body").animate({ scrollTop: 0 }, 600);
1833		});
1834	});
1835	/* ============================================================
1836	   BY THE NUMBERS — STAT EXPAND (width transition)
1837	   ============================================================ */
1838	function initByTheNumbers() {
1839		$(document).on("click", ".by-the-numbers__stat-btn", function () {
1840			var $stat = $(this).closest(".by-the-numbers__stat");
1841			var $allStats = $(".by-the-numbers__stat");
1842			var isOpen = $stat.hasClass("by-the-numbers__stat--expanded");
1843
1844			if (isOpen) {
1845				$allStats.removeClass("by-the-numbers__stat--expanded by-the-numbers__stat--collapsed");
1846				$allStats.find(".by-the-numbers__stat-btn").attr("aria-expanded", "false");
1847			} else {
1848				$allStats.each(function () {
1849					if ($(this).is($stat)) return;
1850					$(this).removeClass("by-the-numbers__stat--expanded").addClass("by-the-numbers__stat--collapsed");
1851				});
1852				$stat.removeClass("by-the-numbers__stat--collapsed").addClass("by-the-numbers__stat--expanded");
1853				$stat.find(".by-the-numbers__stat-btn").attr("aria-expanded", "true");
1854			}
1855		});
1856	}
1857
1858	/* ============================================================
1859	   YOUTH MATTERS — typewriter tagline
1860	   ============================================================ */
1861	function initYouthMatters() {
1862		var $section = $(".youth-matters");
1863		
1864		if (!$section.length) return;
1865
1866		var $wordEl = $section.find(".youth-matters__word");
1867		var $verbEl = $section.find(".youth-matters__verb");
1868		var $dynamicEl = $section.find(".youth-matters__dynamic");
1869
1870		var words;
1871		try { words = JSON.parse($section.attr("data-words")); } catch (e) { return; }
1872		if (!words || !words.length) return;
1873
1874		var suffix = $section.data("suffix") || "Matters";
1875		var suffixPlural = $section.data("suffix-plural") || "Matter";
1876
1877		var HOLD_AFTER_TYPE   = 1750;
1878		var DELETE_DURATION   = 400;
1879		var TYPE_PER_CHAR     = 70;
1880		var PAUSE_BEFORE_TYPE = 180;
1881
1882		function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }
1883
1884		async function typeWord(text) {
1885			$dynamicEl.addClass("youth-matters__typing");
1886			for (var i = 1; i <= text.length; i++) {
1887				$wordEl.text(text.slice(0, i));
1888				await sleep(TYPE_PER_CHAR);
1889			}
1890			$dynamicEl.removeClass("youth-matters__typing");
1891		}
1892
1893		async function deleteWord(text) {
1894			$dynamicEl.addClass("youth-matters__typing");
1895			var step = Math.max(15, DELETE_DURATION / Math.max(text.length, 1));
1896			for (var i = text.length - 1; i >= 0; i--) {
1897				$wordEl.text(text.slice(0, i));
1898				await sleep(step);
1899			}
1900			$dynamicEl.removeClass("youth-matters__typing");
1901		}
1902
1903		async function run() {
1904			var i = 0;
1905			while (true) {
1906				var item = words[i % words.length];
1907				$verbEl.text(item.plural ? suffixPlural : suffix);
1908				await typeWord(item.word);
1909				await sleep(HOLD_AFTER_TYPE);
1910				await deleteWord(item.word);
1911				await sleep(PAUSE_BEFORE_TYPE);
1912				i++;
1913			}
1914		}
1915
1916		run();
1917	}
1918
1919})(jQuery);

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.