1document.addEventListener("DOMContentLoaded", function (event) { 2 3 function openHeaderOverlay(menuAPI) { 4 const html = document.querySelector('html'); 5 const body = document.querySelector('body'); 6 const button = document.querySelector('#menu-button'); 7 const overlay = document.querySelector('#headerOverlay'); 8 const header = document.querySelector('header.site--header'); 9 10 if (!button) return; 11 12 const closedText = button.querySelector('.text.closed'); 13 const openText = button.querySelector('.text.open'); 14 const iconLines = button.querySelectorAll('.icon-container span'); 15 16 const topPosition = window.innerWidth >= 1600 ? "18px" : "10px"; 17 18 const tl = gsap.timeline({ 19 paused: true 20 }); 21 22 tl.to(closedText, { 23 yPercent: -100, 24 y: -10, 25 duration: 0.8, 26 ease: "power4.inOut" 27 }, 0); 28 29 tl.to(openText, { 30 yPercent: 0, 31 y: 0, 32 duration: 0.8, 33 ease: "power4.inOut" 34 }, 0); 35 36 tl.to(iconLines, { 37 top: topPosition, 38 duration: 0.6, 39 ease: "power4.inOut" 40 }, 0); 41 42 43 tl.to(iconLines[0], { 44 rotation: 45, 45 duration: 0.6, 46 ease: "power4.inOut" 47 }, 0.4); 48 49 tl.to(iconLines[1], { 50 rotation: 45, 51 duration: 0.6, 52 ease: "power4.inOut" 53 }, 0.4); 54 55 tl.to(iconLines[2], { 56 rotation: -45, 57 duration: 0.6, 58 ease: "power4.inOut" 59 }, 0.4); 60 61 tl.to(iconLines[3], { 62 rotation: -45, 63 duration: 0.6, 64 ease: "power4.inOut" 65 }, 0.4); 66 67 68 button.addEventListener('click', function(e) { 69 e.preventDefault(); 70 71 const isOpen = button.classList.toggle('open'); 72 73 if (overlay) { 74 overlay.classList.toggle('open', isOpen); 75 } 76 77 if (header) { 78 header.classList.toggle('open', isOpen); 79 } 80 81 /* 82 if(html) { 83 html.classList.toggle('noscroll', isOpen); 84 } 85 */ 86 87 // noscroll toggles once the open/close reveal has actually finished, not the 88 // instant the button is clicked â the scrollbar appearing/disappearing shifts 89 // the layout, and doing that while the menu itself is still mid-animation was 90 // an extra visible jolt on top of it. 91 if (isOpen) { 92 tl.play(); 93 menuAPI?.animateMenuIn(() => { 94 if (body) body.classList.add('noscroll'); 95 }); 96 } else { 97 menuAPI?.animateMenuOut(() => { 98 if (body) body.classList.remove('noscroll'); 99 }); 100 tl.reverse(); 101 } 102 }); 103 } 104 105 function initMenuAnimations() { 106 const overlayMenu = document.querySelector("#headerOverlay"); 107 const menu = document.querySelector("#headerOverlay .header-overlay-inner"); 108 if (!menu) return; 109 110 const topLevel = menu.querySelector(".level--1"); 111 const secondLevel = menu.querySelector(".level--2"); 112 const secondLevelItems = secondLevel?.querySelectorAll(".menu-item-content"); 113 const menuImages = document.querySelector(".navigation-images"); 114 const imageItems = menuImages?.querySelectorAll(".image-container"); 115 116 const colors = { 117 original: getComputedStyle(document.getElementById("headerOverlay")).backgroundColor, 118 current: null, 119 }; 120 121 let itemIndex = null; 122 123 function setupMenuItemColors() { 124 topLevel.querySelectorAll(".menu-item").forEach((item) => { 125 if (!item.dataset.color) return; 126 addHoverEvents(item); 127 }); 128 } 129 130 function addHoverEvents(item) { 131 item.addEventListener("mouseenter", () => handleMenuItemHover(item)); 132 item.addEventListener("focus", () => handleMenuItemHover(item)); 133 // No mouseleave/blur handler â the active item (and its second-level content/ 134 // image) should stay put once hovered/focused, rather than reverting the instant 135 // the cursor leaves it. It's only ever replaced by whichever item takes over next. 136 137 item.addEventListener("keydown", (e) => { 138 if (e.key !== "Tab" || e.shiftKey) return; 139 140 const index = parseInt(item.dataset.menuIndex, 10); 141 142 if (isNaN(index)) return; 143 144 const target = secondLevel.querySelector( 145 `.menu-item-content[data-index="${index}"]` 146 ); 147 148 if (!target) return; 149 150 const firstLink = target.querySelector("a"); 151 152 if (!firstLink) return; 153 154 e.preventDefault(); 155 156 handleMenuItemHover(item); 157 158 // Wait until the content is visible, then focus its first link 159 requestAnimationFrame(() => { 160 firstLink.focus(); 161 }); 162 }); 163 164 item.addEventListener("click", (e) => { 165 if (item.getAttribute("href") === "#") e.preventDefault(); 166 // mouseenter/focus alone don't reliably fire on a touch tap, and a 167 // dropdown item is a <button> with nowhere to navigate to on click, so 168 // this is the only way its second-level content ever opens on mobile. 169 handleMenuItemHover(item); 170 }); 171 } 172 173 function setupSecondLevelFocus() { 174 secondLevel.querySelectorAll(".menu-item-content").forEach((content) => { 175 const links = content.querySelectorAll("a"); 176 177 if (!links.length) return; 178 179 const lastLink = links[links.length - 1]; 180 181 lastLink.addEventListener("keydown", (e) => { 182 if (e.key !== "Tab" || e.shiftKey) return; 183 184 e.preventDefault(); 185 186 const index = parseInt(content.dataset.index, 10); 187 188 if (isNaN(index)) return; 189 190 const items = topLevel.querySelectorAll(".menu-item"); 191 const nextItem = items[index + 1]; 192 193 if (nextItem) { 194 nextItem.focus(); 195 } 196 }); 197 }); 198 } 199 200 function handleMenuItemHover(item) { 201 const hoverColor = item.dataset.color; 202 const index = parseInt(item.dataset.menuIndex, 10); 203 204 if (isNaN(index) || itemIndex === index) return; 205 itemIndex = index; 206 207 updateImageContent(index); 208 updateSecondLevelContent(index); 209 210 if (hoverColor && colors.current !== hoverColor) { 211 gsap.to(overlayMenu, { backgroundColor: hoverColor, duration: 0.15, ease: "power2.out" }); 212 colors.current = hoverColor; 213 } 214 215 topLevel.querySelectorAll(".menu-item").forEach((i) => i.classList.remove("menu-item-active")); 216 item.classList.add("menu-item-active"); 217 } 218
219 function resetMenuBackground() { 220 gsap.to(overlayMenu, { backgroundColor: colors.original, duration: 0.2, ease: "power2.out" }); 221 colors.current = null; 222 itemIndex = null; 223 topLevel.querySelectorAll(".menu-item").forEach((i) => i.classList.remove("menu-item-active")); 224 } 225 226 function updateSecondLevelContent(index) { 227 if (!secondLevelItems) return; 228 229 secondLevelItems.forEach((el) => { 230 gsap.set(el, { display: "none", opacity: 0 }); 231 el.classList.remove("active"); 232 }); 233 234 const target = secondLevel.querySelector(`.menu-item-content[data-index="${index}"]`); 235 if (!target) return; 236 237 const links = target.querySelectorAll(".menu-item-content-links a"); 238 const buttons = target.querySelectorAll(".menu-item-content-buttons a"); 239 const subtitles = target.querySelectorAll(".menu-label"); 240 const text = target.querySelector(".custom-text"); 241 242 gsap.set(target, { display: "block", opacity: 1 }); 243 target.classList.add("active"); 244 245 gsap.set(links, { y: 15, scale: 0.95, opacity: 0 }); 246 gsap.set(buttons, { y: 15, scale: 0.95, opacity: 0 }); 247 gsap.set(subtitles, { y: 15, scale: 0.95 }); 248 gsap.set(text, { y: 15, scale: 0.95 }); 249 250 gsap.to(links, { 251 y: 0, 252 scale: 1, 253 opacity: 1, 254 duration: 0.4, 255 ease: "power2.out", 256 stagger: 0.1, 257 onComplete: () => gsap.set(links, { clearProps: "all" }), 258 }); 259 260 gsap.to(buttons, { 261 y: 0, 262 scale: 1, 263 opacity: 1, 264 duration: 0.4, 265 ease: "power2.out", 266 stagger: 0.1, 267 onComplete: () => gsap.set(buttons, { clearProps: "all" }), 268 }); 269 270 gsap.to(subtitles, { 271 y: 0, 272 scale: 1, 273 duration: 0.4, 274 ease: "power2.out", 275 stagger: 0.1, 276 onComplete: () => gsap.set(subtitles, { clearProps: "all" }), 277 }); 278 279 gsap.to(text, { 280 y: 0, 281 scale: 1, 282 duration: 0.4, 283 ease: "power2.out", 284 stagger: 0.1, 285 onComplete: () => gsap.set(text, { clearProps: "all" }), 286 }); 287 } 288 289 function updateImageContent(index) { 290 if (!imageItems) return; 291 292 imageItems.forEach((img) => { 293 if (parseInt(img.dataset.index, 10) !== index) { 294 gsap.set(img, { display: "none", opacity: 0 }); 295 img.classList.remove("active"); 296 } 297 }); 298 299 const target = menuImages.querySelector(`.image-container[data-index="${index}"]`); 300 if (!target) return; 301 gsap.set(target, { display: "block", opacity: 0 }); 302 target.classList.add("active"); 303 gsap.to(target, { opacity: 1, duration: 0.3, ease: "power2.in" }); 304 } 305 306 function animateMenuIn(onComplete) { 307 const items = topLevel.querySelectorAll(".menu-item"); 308 const subtitle = topLevel.querySelector(".menu-label"); 309 const delay = 0.6; 310 311 gsap.set(items, { y: 30, x: 50, opacity: 0 }); 312 gsap.set(subtitle, { y: 30, x: 50, opacity: 0 }); 313 314 // Wrapped in a timeline (rather than two standalone tweens) so onComplete fires 315 // exactly once, after the slower of the two â attaching it directly to a staggered 316 // tween's own vars fires it once per staggered item, not once overall. 317 const tl = gsap.timeline({ onComplete }); 318 319 tl.to(items, { 320 y: 0, x: 0, opacity: 1, duration: 0.3, ease: "power2.out", 321 stagger: { amount: 0.25, from: "start" }, 322 }, delay); 323 324 tl.to(subtitle, { y: 0, x: 0, opacity: 0.6, duration: 0.4, ease: "power2.out" }, delay); 325 326 // First item active by default whenever the menu opens, matching the 327 // hover-driven state (image/second-level content/background) rather than 328 // opening with nothing selected. 329 if (items.length) handleMenuItemHover(items[0]); 330 } 331 332 function animateMenuOut(onComplete) { 333 const items = topLevel.querySelectorAll(".menu-item"); 334 const subtitle = topLevel.querySelector(".menu-label"); 335 336 const tl = gsap.timeline({ onComplete }); 337 338 tl.to(items, { 339 x: 50, opacity: 0, duration: 0.3, ease: "power2.in", 340 stagger: { amount: 0.15, from: "end" }, 341 }, 0); 342 343 tl.to(subtitle, { x: 50, opacity: 0, duration: 0.3, ease: "power2.in" }, 0); 344
345 resetMenuBackground(); 346 } 347 348 setupMenuItemColors(); 349 setupSecondLevelFocus(); 350 351 return { resetMenuBackground, animateMenuIn, animateMenuOut }; 352 } 353 354 function openMobileSubMenu() { 355 document.querySelectorAll('#headerOverlay .main-link + button').forEach(button => { 356 const submenu = button.nextElementSibling; 357 358 gsap.set(submenu, { 359 height: 0, 360 opacity: 0, 361 overflow: 'hidden', 362 display: 'none' 363 }); 364 365 let isOpen = false; 366 367 button.addEventListener('click', (e) => { 368 e.preventDefault(); 369 button.classList.toggle('open'); 370 371 if (isOpen) { 372 gsap.to(submenu, { 373 height: 0, 374 opacity: 0, 375 duration: 0.3, 376 ease: 'power2.inOut', 377 onComplete: () => { 378 gsap.set(submenu, { display: 'none' }); 379 } 380 }); 381 } else { 382 gsap.set(submenu, { 383 display: 'block', 384 height: 'auto' 385 }); 386 387 const height = submenu.offsetHeight; 388 389 gsap.set(submenu, { 390 height: 0, 391 opacity: 0 392 }); 393 394 gsap.to(submenu, { 395 height, 396 opacity: 1, 397 duration: 0.3, 398 ease: 'power2.inOut', 399 onComplete: () => { 400 gsap.set(submenu, { height: 'auto' }); 401 } 402 }); 403 } 404 405 isOpen = !isOpen; 406 button.setAttribute('aria-expanded', isOpen); 407 }); 408 }); 409 } 410 411 const menuAPI = initMenuAnimations(); 412 openHeaderOverlay(menuAPI); 413 openMobileSubMenu(); 414});
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.