1const header = document.querySelector(".site-header");
2const nav = document.querySelector("#site-nav");
3const navToggle = document.querySelector(".nav-toggle");
4const revealTargets = document.querySelectorAll("[data-reveal]");
5const navLinks = document.querySelectorAll(".site-nav a");
6const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
7
8const setHeaderState = () => {
9 header.classList.toggle("is-scrolled", window.scrollY > 12);
10};
11
12setHeaderState();
13window.addEventListener("scroll", setHeaderState, { passive: true });
14
15if (navToggle && nav) {
16 navToggle.addEventListener("click", () => {
17 const isOpen = navToggle.getAttribute("aria-expanded") === "true";
18 navToggle.setAttribute("aria-expanded", String(!isOpen));
19 nav.classList.toggle("is-open", !isOpen);
20 });
21
22 nav.addEventListener("click", (event) => {
23 if (event.target instanceof HTMLAnchorElement) {
24 navToggle.setAttribute("aria-expanded", "false");
25 nav.classList.remove("is-open");
26 }
27 });
28}
29
30const revealObserver = new IntersectionObserver(
31 (entries, observer) => {
32 entries.forEach((entry) => {
33 if (!entry.isIntersecting) return;
34 entry.target.classList.add("is-visible");
35 observer.unobserve(entry.target);
36 });
37 },
38 { threshold: 0.16, rootMargin: "0px 0px -8% 0px" }
39);
40
41revealTargets.forEach((target) => {
42 const section = target.closest("section");
43 const localTargets = section
44 ? Array.from(section.querySelectorAll("[data-reveal]"))
45 : Array.from(revealTargets);
46 const localIndex = Math.max(localTargets.indexOf(target), 0);
47 const delay = Math.min(localIndex, 4) * 110;
48
49 target.style.setProperty("--reveal-delay", `${delay}ms`);
50 revealObserver.observe(target);
51});
52
53window.setTimeout(() => {
54 revealTargets.forEach((target) => target.classList.add("is-visible"));
55}, 1600);
56
57const sections = Array.from(document.querySelectorAll("main section[id]"));
58let activeNavFrame = 0;
59
60const setActiveNavLink = (sectionId) => {
61 navLinks.forEach((link) => {
62 const isActive = link.getAttribute("href") === `#${sectionId}`;
63 if (isActive) {
64 link.setAttribute("aria-current", "page");
65 } else {
66 link.removeAttribute("aria-current");
67 }
68 });
69};
70
71const getCurrentSectionId = () => {
72 const anchorY = (header?.offsetHeight ?? 0) + Math.min(window.innerHeight * 0.32, 260);
73 let currentSection = sections[0];
74
75 for (const section of sections) {
76 const rect = section.getBoundingClientRect();
77 if (rect.top <= anchorY && rect.bottom > anchorY) {
78 return section.id;
79 }
80
81 if (rect.top <= anchorY) {
82 currentSection = section;
83 }
84 }
85
86 return currentSection?.id;
87};
88
89const updateActiveNav = () => {
90 activeNavFrame = 0;
91 const currentSectionId = getCurrentSectionId();
92 if (currentSectionId) {
93 setActiveNavLink(currentSectionId);
94 }
95};
96
97const requestActiveNavUpdate = () => {
98 if (activeNavFrame) return;
99 activeNavFrame = window.requestAnimationFrame(updateActiveNav);
100};
101
102updateActiveNav();
103window.addEventListener("scroll", requestActiveNavUpdate, { passive: true });
104window.addEventListener("resize", requestActiveNavUpdate);
105window.addEventListener("hashchange", requestActiveNavUpdate);
106
107const speakerCarousel = document.querySelector(".speaker-carousel");
108
109if (speakerCarousel) {
110 const speakerTrack = speakerCarousel.querySelector(".speaker-track");
111 const speakerSlides = Array.from(speakerCarousel.querySelectorAll(".speaker-slide"));
112 const speakerDots = Array.from(speakerCarousel.querySelectorAll(".speaker-dot"));
113 let activeSpeakerIndex = 0;
114 let speakerTimer = null;
115
116 const setSpeakerSlide = (index) => {
117 activeSpeakerIndex = (index + speakerSlides.length) % speakerSlides.length;
118 speakerCarousel.style.setProperty("--speaker-index", activeSpeakerIndex);
119 speakerCarousel.style.setProperty("--speaker-offset", `${activeSpeakerIndex * -100}%`);
120
121 speakerSlides.forEach((slide, slideIndex) => {
122 const isActive = slideIndex === activeSpeakerIndex;
123 slide.classList.toggle("is-active", isActive);
124 slide.toggleAttribute("aria-hidden", !isActive);
125 });
126
127 speakerDots.forEach((dot, dotIndex) => {
128 const isActive = dotIndex === activeSpeakerIndex;
129 dot.classList.toggle("is-active", isActive);
130 if (isActive) {
131 dot.setAttribute("aria-current", "true");
132 } else {
133 dot.removeAttribute("aria-current");
134 }
135 });
136 };
137
138 const stopSpeakerTimer = () => {
139 if (!speakerTimer) return;
140 window.clearInterval(speakerTimer);
141 speakerTimer = null;
142 };
143
144 const startSpeakerTimer = () => {
145 if (reducedMotion.matches || speakerSlides.length < 2 || speakerTimer) return;
146 speakerTimer = window.setInterval(() => {
147 setSpeakerSlide(activeSpeakerIndex + 1);
148 }, 5600);
149 };
150
151 speakerDots.forEach((dot) => {
152 dot.addEventListener("click", () => {
153 const slideIndex = Number(dot.dataset.slide);
154 if (Number.isNaN(slideIndex)) return;
155 setSpeakerSlide(slideIndex);
156 stopSpeakerTimer();
157 startSpeakerTimer();
158 });
159 });
160
161 speakerCarousel.addEventListener("pointerenter", stopSpeakerTimer);
162 speakerCarousel.addEventListener("pointerleave", startSpeakerTimer);
163 speakerCarousel.addEventListener("focusin", stopSpeakerTimer);
164 speakerCarousel.addEventListener("focusout", startSpeakerTimer);
165 speakerCarousel.addEventListener("keydown", (event) => {
166 if (event.key === "ArrowDown" || event.key === "ArrowRight") {
167 event.preventDefault();
168 setSpeakerSlide(activeSpeakerIndex + 1);
169 }
170
171 if (event.key === "ArrowUp" || event.key === "ArrowLeft") {
172 event.preventDefault();
173 setSpeakerSlide(activeSpeakerIndex - 1);
174 }
175 });
176
177 if (speakerTrack) {
178 setSpeakerSlide(0);
179 startSpeakerTimer();
180 }
181}