1( function () { 2 3 // Swoop path definitions â add more shapes here as needed. 4 // Each path's `length` must match the actual SVG path length, 5 // or leave it null to have JS measure it automatically. 6 const SWOOPS = { 7 'swoop-default': { 8 viewBox: '0 0 500 150', 9 d: 'M7.7,145.6C109,125,299.9,116.2,401,121.3c42.1,2.2,87.6,11.8,87.3,25.7', 10 }, 11 'swoop-straight': { 12 viewBox: '0 0 500 50', 13 d: 'M0,25 Q250,5 500,25', 14 }, 15 'swoop-double': { 16 viewBox: '0 0 500 80', 17 d: 'M5,30 Q250,5 495,30 M5,55 Q250,30 495,55', 18 }, 19 }; 20 21 function getSwoopKey( el ) { 22 for ( const key of Object.keys( SWOOPS ) ) { 23 if ( el.classList.contains( key ) ) return key; 24 } 25 return 'swoop-default'; 26 } 27 28 function buildSVG( swoop ) { 29 const svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ); 30 svg.setAttribute( 'viewBox', swoop.viewBox ); 31 svg.setAttribute( 'preserveAspectRatio', 'none' ); 32 svg.setAttribute( 'aria-hidden', 'true' ); 33 svg.classList.add( 'swoop-svg' ); 34 35 // Support multiple paths (e.g. swoop-double) 36 const paths = Array.isArray( swoop.d ) ? swoop.d : [ swoop.d ]; 37 paths.forEach( d => { 38 const path = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' ); 39 path.setAttribute( 'd', d ); 40 svg.appendChild( path ); 41 } ); 42 43 return svg; 44 } 45 46 function initSwoop( heading ) { 47 const strong = heading.querySelector( 'strong' ); 48 if ( ! strong ) return; 49 50 const swoopKey = getSwoopKey( heading ); 51 const swoop = SWOOPS[ swoopKey ]; 52 const svg = buildSVG( swoop ); 53 54 strong.style.position = 'relative'; 55 strong.appendChild( svg ); 56 57 // Wait for the browser to paint before measuring path length. 58 // Double-rAF ensures we're past the style recalc phase. 59 requestAnimationFrame( () => { 60 requestAnimationFrame( () => { 61 svg.querySelectorAll( 'path' ).forEach( path => { 62 const len = path.getTotalLength(); 63 64 // Bail out if measurement still failed 65 if ( ! len ) { 66 path.style.strokeDasharray = ''; 67 path.style.strokeDashoffset = ''; 68 return; 69 } 70// path.style.setProperty( '--swoop-path-length', len ); 71 } ); 72 73 // Start observing only after the path is correctly hidden. 74 // This also prevents the "flash" on headings already in the viewport. 75 heading.classList.add( 'is-ready' ); 76 observe( heading ); 77 } ); 78 } ); 79 } 80 81 function observe( heading ) { 82 const io = new IntersectionObserver( 83 ( entries ) => { 84 entries.forEach( entry => { 85 if ( entry.isIntersecting ) { 86 heading.classList.add( 'is-visible' ); 87 io.unobserve( heading ); // fire once 88 } 89 } ); 90 }, 91 { threshold: 0.3 } 92 ); 93 io.observe( heading ); 94 } 95 96 document.addEventListener( 'DOMContentLoaded', () => { 97 document.querySelectorAll( '.is-style-has-swoop' ).forEach( heading => { 98 initSwoop( heading ); 99 } ); 100 101 moveBlogPrefooter(); 102 initMobileDrawer(); 103 } ); 104 105 // ââ Megamenu ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 106 107 const navItems = document.querySelectorAll( '.wp-block-navigation-item.has-megamenu' ); 108 const panels = document.querySelectorAll( '.megamenu-panel' ); 109 const container = document.querySelector( '.megamenu-panels' ); 110 111 // Pending open/close timers â cancelled when the cursor reverses direction. 112 let openTimer = null; 113 let closeTimer = null; 114 115 function openPanel( name ) { 116 if ( window.innerWidth < 1024 ) return; 117 closeAll(); 118 119 const panel = document.querySelector( `.megamenu-panel[data-menu="${ name }"]` ); 120 const trigger = document.querySelector( 121 `.wp-block-navigation-item.has-megamenu[data-menu="${ name }"], .megamenu--${ name }` 122 ); 123 if ( ! panel ) return; 124 125 panel.style.opacity = '0'; 126 panel.style.display = 'block'; 127 const targetHeight = panel.scrollHeight; 128 panel.style.display = ''; 129 panel.style.opacity = ''; 130 131 container.style.height = `${ targetHeight }px`; 132 panel.classList.add( 'is-open' ); 133 trigger?.setAttribute( 'aria-expanded', 'true' ); 134 } 135
136 function scheduleOpen( name ) { 137 const alreadyOpen = container?.querySelector( '.megamenu-panel.is-open' ); 138 139 // No panel open: open immediately â deliberate hover, no diagonal-travel risk. 140 if ( ! alreadyOpen ) { 141 cancelOpen(); 142 openPanel( name ); 143 return; 144 } 145 146 // Panel already open: delay to absorb accidental corner-brushing while the 147 // user moves their cursor diagonally from the nav toward the panel below. 148 cancelOpen(); 149 openTimer = setTimeout( () => openPanel( name ), 160 ); 150 } 151 152 function cancelOpen() { 153 clearTimeout( openTimer ); 154 openTimer = null; 155 } 156 157 function scheduleClose() { 158 clearTimeout( closeTimer ); 159 closeTimer = setTimeout( closeAll, 150 ); 160 } 161 162 function cancelClose() { 163 clearTimeout( closeTimer ); 164 closeTimer = null; 165 } 166 167 function closeAll() { 168 cancelOpen(); // prevent a delayed open from firing after close 169 cancelClose(); // prevent a re-entrant scheduled close 170 panels.forEach( p => p.classList.remove( 'is-open' ) ); 171 navItems.forEach( n => n.setAttribute( 'aria-expanded', 'false' ) ); 172 if ( container ) container.style.height = ''; 173 } 174 175 navItems.forEach( item => { 176 const name = item.dataset.menu 177 ?? [ ...item.classList ] 178 .find( c => c.startsWith( 'megamenu--' ) ) 179 ?.replace( 'megamenu--', '' ); 180 181 if ( ! name ) return; 182 183 item.addEventListener( 'mouseenter', () => scheduleOpen( name ) ); 184 item.addEventListener( 'mouseleave', cancelOpen ); // cursor left before delay fired 185 186 item.querySelector( 'a' )?.addEventListener( 'keydown', e => { 187 if ( e.key !== 'Enter' && e.key !== ' ' ) return; 188 const isOpen = item.getAttribute( 'aria-expanded' ) === 'true'; 189 if ( isOpen ) { 190 closeAll(); 191 } else { 192 e.preventDefault(); 193 openPanel( name ); 194 document.querySelector( `.megamenu-panel[data-menu="${ name }"]` ) 195 ?.querySelector( 'a, button' ) 196 ?.focus(); 197 } 198 } ); 199 } ); 200 201 document.addEventListener( 'keydown', e => { 202 if ( e.key === 'Escape' ) closeAll(); 203 } ); 204 205 document.addEventListener( 'click', e => { 206 if ( ! e.target.closest( '.site-header' ) ) closeAll(); 207 } ); 208 209 // Delay close when the cursor leaves the header so brief exits don't collapse 210 // the panel (e.g. overshooting the nav bar slightly on the way to a panel link). 211 document.querySelector( '.site-header' )?.addEventListener( 'mouseleave', scheduleClose ); 212 document.querySelector( '.site-header' )?.addEventListener( 'mouseenter', cancelClose ); 213 214 // Arrow keys within an open panel move between focusable items. 215 container?.addEventListener( 'keydown', e => { 216 if ( ! [ 'ArrowDown', 'ArrowUp' ].includes( e.key ) ) return; 217 const openPanel = container.querySelector( '.megamenu-panel.is-open' ); 218 if ( ! openPanel ) return; 219 220 const focusable = [ ...openPanel.querySelectorAll( 'a, button' ) ]; 221 const index = focusable.indexOf( document.activeElement ); 222 223 if ( e.key === 'ArrowDown' ) { 224 e.preventDefault(); 225 focusable[ Math.min( index + 1, focusable.length - 1 ) ]?.focus(); 226 } else if ( e.key === 'ArrowUp' ) { 227 e.preventDefault(); 228 if ( index <= 0 ) { 229 const activeMenu = openPanel.dataset.menu; 230 closeAll(); 231 document.querySelector( 232 `.wp-block-navigation-item.has-megamenu[data-menu="${ activeMenu }"] a, 233 .megamenu--${ activeMenu } a` 234 )?.focus(); 235 } else { 236 focusable[ index - 1 ]?.focus(); 237 } 238 } 239 } ); 240 241 // ââ Prefooter gradient + blog-post relocation ââââââââââââââââââââââââââââ 242 // 243 // On non-blog pages the .prefooter block is already adjacent to .site-footer 244 // in the template HTML; updateFooterGradient() just sizes the CSS gradient. 245 // 246 // On single blog posts the block lives inside .wp-block-post-content (it's 247 // the last block in post_content). moveBlogPrefooter() lifts it out and 248 // inserts it immediately before .site-footer, producing the same visual 249 // result as non-blog pages. CSS hides it while it's still in the wrong 250 // position (see momentive.scss â .wp-block-post-content .prefooter). 251 252 function moveBlogPrefooter() { 253 const prefooter = document.querySelector( '.wp-block-post-content .prefooter' ); 254 if ( ! prefooter ) return; 255 const footer = document.querySelector( '.site-footer' ); 256 if ( ! footer ) return; 257 258 footer.parentNode.insertBefore( prefooter, footer ); 259 260 // Remove the hide rule now that the element is in the right place, 261 // then recalculate the gradient with the correct height. 262 prefooter.style.removeProperty( 'display' ); 263 updateFooterGradient(); 264 } 265 266 function updateFooterGradient() { 267 const prefooter = document.querySelector( '.prefooter' ); 268 const footer = document.querySelector( '.site-footer' ); 269 if ( ! footer ) return; 270 271 // Measure the actual distance from the footer's top edge to the 272 // prefooter's top edge â this captures both the prefooter's height 273 // and any gap between the two elements (e.g. a margin-top on the 274 // footer's block wrapper in the simple/landing footer variant). 275 let extraHeight = 0; 276 if ( prefooter ) { 277 const footerTop = footer.getBoundingClientRect().top + window.scrollY; 278 const prefooterTop = prefooter.getBoundingClientRect().top + window.scrollY; 279 extraHeight = Math.max( 0, footerTop - prefooterTop ); 280 } 281 282 footer.style.setProperty( '--gradient-overshoot', `calc(100% + ${ extraHeight }px)` ); 283 } 284 285 updateFooterGradient(); 286 window.addEventListener( 'resize', updateFooterGradient ); 287 288 // Re-run whenever the document height changes (e.g. a HubSpot form in the 289 // hero loads late and pushes content down after the initial measurement). 290 if ( typeof ResizeObserver !== 'undefined' ) { 291 new ResizeObserver( updateFooterGradient ).observe( document.body ); 292 } 293 294 // ââ Mobile drawer navigation âââââââââââââââââââââââââââââââââââââââââââââââ 295 // 296 // On tablet/mobile the responsive overlay shows top-level nav items. 297 // For items with a megamenu, clicking the row slides the primary list 298 // offscreen left and slides in a secondary panel (cloned from the matching 299 // .megamenu-panel) from the right. A "â Back" button returns to the list; 300 // a "View All [Section]" link at the bottom links to the section's top page. 301 302 function initMobileDrawer() { 303 const nav = document.querySelector( '.wp-block-navigation' ); 304 if ( ! nav ) return; 305
306 const overlay = nav.querySelector( '.wp-block-navigation__responsive-container' ); 307 if ( ! overlay ) return; 308 309 const content = overlay.querySelector( '.wp-block-navigation__responsive-container-content' ); 310 if ( ! content ) return; 311 312 const primaryList = content.querySelector( 'ul.wp-block-navigation__container' ); 313 if ( ! primaryList ) return; 314 315 // Build the drawer shell â two panels side-by-side inside a clipping wrapper. 316 const drawer = document.createElement( 'div' ); 317 drawer.className = 'mobile-drawer'; 318 319 const primary = document.createElement( 'div' ); 320 primary.className = 'mobile-drawer__primary'; 321 322 const secondary = document.createElement( 'div' ); 323 secondary.className = 'mobile-drawer__secondary'; 324 secondary.setAttribute( 'aria-hidden', 'true' ); 325 326 primaryList.parentNode.insertBefore( drawer, primaryList ); 327 primary.appendChild( primaryList ); 328 drawer.appendChild( primary ); 329 drawer.appendChild( secondary ); 330 331 // For each megamenu item, add a dedicated arrow toggle button. 332 // The link itself navigates normally; only the button opens the drawer. 333 nav.querySelectorAll( '.wp-block-navigation-item.has-megamenu' ).forEach( item => { 334 const link = item.querySelector( ':scope > .wp-block-navigation-item__content' ); 335 if ( ! link ) return; 336 337 const name = item.dataset.menu 338 ?? [ ...item.classList ] 339 .find( c => c.startsWith( 'megamenu--' ) ) 340 ?.replace( 'megamenu--', '' ); 341 if ( ! name ) return; 342 343 const label = ( link.querySelector( '.wp-block-navigation-item__label' )?.textContent 344 ?? link.textContent ).trim(); 345 346 const toggle = document.createElement( 'button' ); 347 toggle.className = 'mobile-drawer-toggle'; 348 toggle.setAttribute( 'aria-label', `Open ${ label } submenu` ); 349 item.appendChild( toggle ); 350 351 toggle.addEventListener( 'click', e => { 352 e.stopPropagation(); 353 openMobileSubpanel( drawer, secondary, name, toggle ); 354 } ); 355 } ); 356 357 // Back button â delegated so it works after innerHTML is replaced. 358 secondary.addEventListener( 'click', e => { 359 const back = e.target.closest( '.mobile-drawer-back' ); 360 if ( ! back ) return; 361 closeMobileSubpanel( drawer, secondary ); 362 secondary._opener?.focus(); 363 } ); 364 365 // Reset the drawer whenever the overlay is closed. 366 overlay.querySelector( '.wp-block-navigation__responsive-container-close' ) 367 ?.addEventListener( 'click', () => closeMobileSubpanel( drawer, secondary, true ) ); 368 } 369 370 function openMobileSubpanel( drawer, secondary, name, opener ) { 371 const panel = document.querySelector( `.megamenu-panel[data-menu="${ name }"]` ); 372 if ( ! panel ) return; 373 374 secondary._opener = opener; 375 376 // Clone first so we can extract the .read-more link before injecting. 377 const cloned = panel.cloneNode( true ); 378 const readMore = cloned.querySelector( '.read-more a' ); 379 const viewAll = readMore 380 ? `<a class="mobile-drawer__view-all" href="${ readMore.getAttribute( 'href' ) }">${ readMore.textContent.trim() } <span aria-hidden="true">â</span></a>` 381 : ''; 382 383 secondary.innerHTML = ` 384 <div class="mobile-drawer__secondary-inner"> 385 <button class="mobile-drawer-back" aria-label="Back to main menu"> 386 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" focusable="false"> 387 <path d="M10 3L5 8l5 5" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/> 388 </svg> 389 Back 390 </button> 391 <div class="mobile-drawer__panel-content"></div> 392 ${ viewAll } 393 </div> 394 `; 395 396 // .read-more is already hidden by the mobile panel CSS; no need to remove it from clone. 397 secondary.querySelector( '.mobile-drawer__panel-content' ).appendChild( cloned ); 398 399 // Reading a layout property forces a reflow so the browser registers 400 // the secondary panel's starting position before the class change that 401 // triggers the CSS transition. Without this, both DOM mutations land in 402 // the same frame and there's no "from" state to animate from. 403 void secondary.offsetWidth; 404 405 drawer.classList.add( 'is-secondary-open' ); 406 secondary.setAttribute( 'aria-hidden', 'false' ); 407 secondary.querySelector( '.mobile-drawer-back' )?.focus(); 408 } 409 410 function closeMobileSubpanel( drawer, secondary, instant = false ) { 411 drawer.classList.remove( 'is-secondary-open' ); 412 secondary.setAttribute( 'aria-hidden', 'true' ); 413 414 if ( instant ) { 415 secondary.innerHTML = ''; 416 } else { 417 secondary.addEventListener( 'transitionend', () => { 418 if ( ! drawer.classList.contains( 'is-secondary-open' ) ) { 419 secondary.innerHTML = ''; 420 } 421 }, { once: true } ); 422 } 423 } 424 425 // Sticky-bottom bar â slides up from below once scrolled to the bar's 426 // intended DOM position, stays fixed at the bottom, slides out near footer. 427 // Move the block in the editor to control where it "scrolls in" from; 428 // the bar's DOM position is used as the trigger, not any specific sentinel class. 429 const bar = document.querySelector( '.sticks-to-bottom' ); 430 431 if ( bar ) { 432 // Insert a 0-height marker at the bar's DOM position. 433 // position:fixed takes the bar out of flow, so we need this in-flow 434 // reference to know the scroll offset that should trigger the bar. 435 const marker = document.createElement( 'div' ); 436 marker.setAttribute( 'aria-hidden', 'true' ); 437 marker.style.cssText = 'height:0;overflow:hidden;pointer-events:none;'; 438 bar.parentNode.insertBefore( marker, bar ); 439 440 const barH = bar.offsetHeight; 441 const rem = parseFloat( getComputedStyle( document.documentElement ).fontSize ); 442 const exitEl = document.querySelector( '.prefooter' ) || document.querySelector( '.site-footer' ); 443 444 const updateBar = () => { 445 const vpBottom = window.scrollY + window.innerHeight; 446 const markerDocY = marker.getBoundingClientRect().top + window.scrollY - barH; 447 const exitDocY = exitEl 448 ? exitEl.getBoundingClientRect().top + window.scrollY 449 : Infinity; 450 451 // Slide in: when viewport bottom reaches the bar's natural bottom 452 // (marker + barH) plus the 2rem bottom offset â seamless handoff. 453 // Slide out: when the prefooter (or footer if no prefooter) enters the viewport. 454 const shouldStick = vpBottom >= markerDocY + barH + rem * 2 455 && vpBottom < exitDocY; 456 457 bar.classList.toggle( 'is-stuck', shouldStick ); 458 }; 459 460 window.addEventListener( 'scroll', updateBar, { passive: true } ); 461 updateBar(); // handle page loads where already scrolled 462 } 463 464} () );
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.