script.js

  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}