1var month3Ltr = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; 2 3function assignIframeClassW(iframeID) { 4 var iframeW = jQuery('iframe#'+iframeID).width(); 5 //console.log('iframeW',iframeW); 6 var selClass = ''; 7 if(iframeW < 500) { 8 selClassNum = 'sm'; 9 } else if (iframeW >= 500) { 10 selClassNum = 'md'; 11 } 12 jQuery('iframe#'+iframeID).attr('data-size',selClassNum); 13} 14 15function setCookie(cname, cvalue, exdays) { 16 var d = new Date(); 17 d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); 18 var expires = "expires="+d.toUTCString(); 19 document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; 20} 21 22function getCookie(cname) { 23 var name = cname + "="; 24 var ca = document.cookie.split(';'); 25 for(var i = 0; i < ca.length; i++) { 26 var c = ca[i]; 27 while (c.charAt(0) == ' ') { 28 c = c.substring(1); 29 } 30 if (c.indexOf(name) == 0) { 31 return c.substring(name.length, c.length); 32 } 33 } 34 return ""; 35} 36 37function checkCookie() { 38 var user = getCookie("username"); 39 if (user != "") { 40 alert("Welcome again " + user); 41 } else { 42 user = prompt("Please enter your name:", ""); 43 if (user != "" && user != null) { 44 setCookie("username", user, 365); 45 } 46 } 47} 48 49function isJson(str) { 50 try { 51 JSON.parse(str); 52 } catch (e) { 53 return false; 54 } 55 return true; 56} 57 58function getTodayDate() { 59 var today = new Date(); 60 var dd = String(today.getDate()).padStart(2, '0'); 61 var mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! 62 var yyyy = today.getFullYear(); 63 64 return mm + '/' + dd + '/' + yyyy; 65} 66 67function getTodayDateOrUrlParam(urlParam) { 68 var selUrlParam = decodeURIComponent(getUrlParam(urlParam,'')); 69 var today = new Date(); 70 var dd = String(today.getDate()).padStart(2, '0'); 71 var mm = String(today.getMonth() + 1).padStart(2, '0'); //January is 0! 72 var yyyy = today.getFullYear(); 73 74 return selUrlParam.length ? selUrlParam : mm + '/' + dd + '/' + yyyy; 75} 76 77var formatter = new Intl.NumberFormat('en-US', { 78 style: 'currency', 79 currency: 'USD', 80}); 81 82function getUrlVars() { 83 var vars = {}; 84 var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { 85 vars[key] = value; 86 }); 87 return vars; 88} 89 90function getUrlParam(parameter, defaultvalue){ 91 var urlparameter = defaultvalue; 92 if(window.location.href.indexOf(parameter) > -1){ 93 urlparameter = getUrlVars()[parameter]; 94 } 95 return urlparameter; 96} 97 98function getTimeOfDayGreeting() { 99 const now = new Date(); 100 const hour = now.getHours(); // Get the current hour (0-23) 101 102 if (hour >= 5 && hour < 12) { 103 return "GOOD MORNING"; 104 } else if (hour >= 12 && hour < 18) { 105 return "GOOD AFTERNOON"; 106 } else { 107 return "GOOD EVENING"; 108 } 109} 110 111function addOnClickAnchor(id) { 112 /*jQuery( '#' + id + ' a.anchor-to-deck' ).on('click', function(e) { 113 e.preventDefault(); 114 if (this.hash !== "") { 115 jQuery('html, body').animate({ 116 scrollTop: jQuery(this.hash).offset().top - document.getElementById('masthead').offsetHeight 117 }, 800, function(e){ 118 }); 119 } 120 });*/ 121} 122 123(function ($) { 124 'use strict'; 125 126 $(document).ready(function () { 127 128 //------------ Offcanvas menu ------------- 129 130// $('.open__menu').on('click', function() { 131// $(',main__menu, .overlay').addClass('active'); 132// }) 133// $('..close__menu, .overlay').on('click', function() { 134// $('.main__menu, .overlay').removeClass('active'); 135// }) 136 137 AOS.init({ 138 // Global settings: 139 disable: false, // accepts following values: 'phone', 'tablet', 'mobile', boolean, expression or function 140 startEvent: 'DOMContentLoaded', // name of the event dispatched on the document, that AOS should initialize on 141 initClassName: 'aos-init', // class applied after initialization 142 animatedClassName: 'aos-animate', // class applied on animation 143 useClassNames: false, // if true, will add content of `data-aos` as classes on scroll 144 disableMutationObserver: false, // disables automatic mutations' detections (advanced) 145 debounceDelay: 50, // the delay on debounce used while resizing window (advanced) 146 throttleDelay: 99, // the delay on throttle used while scrolling the page (advanced) 147 148 149 // Settings that can be overridden on per-element basis, by `data-aos-*` attributes: 150 offset: 120, // offset (in px) from the original trigger point 151 delay: 0, // values from 0 to 3000, with step 50ms 152 duration: 400, // values from 0 to 3000, with step 50ms 153 easing: 'ease', // default easing for AOS animations 154 once: true, // whether animation should happen only once - while scrolling down 155 mirror: false, // whether elements should animate out while scrolling past them 156 anchorPlacement: 'top-bottom', // defines which position of the element regarding to window should trigger the animation 157 158 }); 159 160 $('.open-menu-bar').on('click', function (e) { 161 e.preventDefault(); 162 $('body').addClass('overflow-hidden'); 163 $('.off-canvas-section').addClass('active'); 164 $('.off-canvas-wrap').addClass('active'); 165 }); 166 167 $(".off-canvas-overlay").on('click', function () { 168 $('body').removeClass('overflow-hidden'); 169 $(this).parent().removeClass('active'); 170 $('.off-canvas-wrap').removeClass('active'); 171 }); 172 173 $(".off-canvas-close").on('click', function (e) { 174 //console.log('off-canvas-close'); 175 $('body').removeClass('overflow-hidden'); 176 $('.off-canvas-section').removeClass('active'); 177 $('.off-canvas-wrap').removeClass('active'); 178 e.preventDefault(); 179 }); 180 181 var adultNumber = 1; 182 var childNumber = 0; 183 var infantNumber = 0; 184 var totalNumber = adultNumber + childNumber + infantNumber; 185 function updateTotalNumber(mode, num) { 186 if(mode === 'adult') { 187 adultNumber = num; 188 $('input#hiddenADNum').val(num); 189 } 190 if(mode === 'child') { 191 childNumber = num; 192 $('input#hiddenCHDNum').val(num); 193 } 194 if(mode === 'infant') { 195 infantNumber = num; 196 $('input#hiddenINFNum').val(num); 197 } 198 totalNumber = adultNumber + childNumber + infantNumber; 199 $('.passenger-current .num').html(totalNumber); 200 } 201 202 $('input#adultNumber').niceNumber({ 203 onIncrement: function($currentInput, amount, settings) { 204 updateTotalNumber('adult', amount); 205 }, 206 onDecrement: function($currentInput, amount, settings) { 207 updateTotalNumber('adult', amount); 208 }, 209 }); 210 $('input#childNumber').niceNumber({ 211 onIncrement: function($currentInput, amount, settings) { 212 updateTotalNumber('child', amount); 213 }, 214 onDecrement: function($currentInput, amount, settings) { 215 updateTotalNumber('child', amount); 216 }, 217 }); 218 $('input#infantNumber').niceNumber({ 219 onIncrement: function($currentInput, amount, settings) { 220 updateTotalNumber('infant', amount); 221 }, 222 onDecrement: function($currentInput, amount, settings) { 223 updateTotalNumber('infant', amount); 224 }, 225 }); 226 227 $(window).click(function(e) { 228 if(screen.width < 768 && $('#samModal').hasClass('show')) { 229 //console.log('clicking on mobile'); 230 $('#samModal').modal('hide'); 231 } 232 //console.log('clicking', $(this)); 233 }); 234 235 $('.current-promo-code').click(function (e) { 236 e.preventDefault(); 237 $(this).parent().toggleClass('active'); 238 }); 239 240 $('.passenger-current').click(function (e) { 241 e.preventDefault(); 242 $(this).parent().toggleClass('active'); 243 }); 244 245 $('form#findBookingForm').on('submit', function(e) { 246 var refNum = $('#tmpReferenceNumber').val(); 247 var tixNum = $('#ticketNumber').val(); 248 var lName = $('#travelerSurname').val(); 249 if(lName.length) { 250 if(tixNum.length) { 251 $('input[name=searchType]').val('TicketNumber'); 252 $('input[name=referenceNumber]').val(tixNum); 253 } else { 254 $('input[name=searchType]').val('PNR'); 255 $('input[name=referenceNumber]').val(refNum); 256 } 257 return true; 258 } else { 259 $('#travelerSurname').focus(); 260 $('#travelerSurname').addClass('required'); 261 } 262 return false;
263 }); 264 265 $('form#mc-embedded-subscribe-form').on('submit', function(e) { 266 var email = $('input#MERGE0').val(); 267 if(email.length) { 268 return true; 269 } 270 $('input#newEmail').focus(); 271 return false; 272 }); 273 274 $('li.nav-login a').on('click', function(e) { 275 e.preventDefault(); 276 $('.off-canvas-wrap').removeClass('active'); 277 $('.off-canvas-section').removeClass('active'); 278 $('body').removeClass('overflow-hidden'); 279 $('#accountModal').modal('show'); 280 }); 281 282 var bannerUrl = 'https://www.surfair.com/wp-content/themes/surfair/json/top-banner.json'; 283 var activateBanner = false; 284 $.ajax({ 285 dataType: "json", 286 url: bannerUrl, 287 success: function (notificationList) { 288 var bannerItem = activateBanner ? notificationList.banner || [] : []; 289 console.log('bannerItem', {bannerItem,notificationList}); 290 if (bannerItem.length) { 291 for(var i = 0; i < bannerItem.length; i++) { 292 let thisBanner = '<div class="banner-item">'; 293 if((bannerItem[i].link || '').length) { 294 thisBanner += '<a href="'+bannerItem[i].link+'" target="_blank">'; 295 } 296 thisBanner += bannerItem[i].text; 297 if((bannerItem[i].link || '').length) { 298 thisBanner += '</a>'; 299 } 300 thisBanner += '</div>'; 301 //console.log('item', bannerItem[i].text); 302 $('.banner-item-wrapper .banner-item-inner').append(thisBanner); 303 } 304 $('.top-banner-wrapper').removeClass('hide'); 305 $('.banner-item-wrapper .banner-item-inner').slick( { 306 arrows: false, 307 autoplay: true, 308 autoplaySpeed: 5000, 309 dots: true, 310 }); 311 } 312 }, 313 error: function (jqXHR, error, errorThrown) { 314 if (jqXHR.status && jqXHR.status == 400) { 315 console.log(jqXHR.responseText); 316 } else { 317 console.log("Something went wrong - ", {error,errorThrown}); 318 } 319 }, 320 }); 321 322 $('.close-banner').on('click', function(e) { 323 e.preventDefault(); 324 $('.top-banner-wrapper').addClass('hide'); 325 }); 326 327 var urlmode = getUrlParam('mode', ''); 328 var legalPageMode = ['legalTOU','legalPP','cookiesPolicy','membershipAgreement','referralBenefits','contractOfCarriage','animalPolicy','surfCredit','onDemandCharter','guaranteedORates','destinationAddOn','specialSale','specialSale','crowdfundedCharter','complimentaryFlight','jetcard']; 329 var legalSurfondemandMode = ['guaranteedORates','onDemandCharter']; 330 if(legalSurfondemandMode.includes(urlmode)) { 331 location.href = 'https://www.surfondemand.com/legal/'; 332 } else if(legalPageMode.includes(urlmode)) { 333 $('#' + urlmode).addClass('vc_toggle_active'); 334 $('#' + urlmode + ' .vc_toggle_content').attr('style', 'display: block;'); 335 console.log('legal page'); 336 jQuery('html, body').animate({ 337 scrollTop: jQuery('#'+urlmode).offset().top - document.getElementById('masthead').offsetHeight 338 }, 800, function(e){ 339 console.log('end'); 340 }); 341 } 342 343 $('a.anchor-to-deck').on('click', function(e) { 344 e.preventDefault(); 345 event.stopPropagation(); 346 //console.log('star'); 347 jQuery('html, body').animate({ 348 scrollTop: jQuery(this.hash).offset().top - ((document.getElementById('masthead') || {}).offsetHeight || 0) - 100 349 }, 800, function(e){ 350 //console.log('end'); 351 }); 352 }); 353 354 $(".tab-function").click(function(e){ 355 e.preventDefault(); 356 if(!$(this).hasClass('active')) { 357 var thisGroup = $(this).attr('data-group'); 358 var thisId = $(this).attr('data-id'); 359 $('ul.tab-list[group-id='+thisGroup+'] li a').removeClass('active'); 360 $(this).addClass('active'); 361 console.log('clicked', {thisGroup, thisId}); 362 //$('ul.glisting#'+thisGroup+' li').attr('display', ''); 363 //$('ul.glisting#'+thisGroup+' li.active').removeClass('active'); 364 $('ul.glisting#'+thisGroup+' li.active').fadeOut(250, function() { 365 $('ul.glisting#'+thisGroup+' li.active').removeClass('active'); 366 $('ul.glisting#'+thisGroup+' li.'+thisId).addClass('active'); 367 $('ul.glisting#'+thisGroup+' li.'+thisId).fadeIn(250); 368 }); 369 //$('ul.glisting#'+thisGroup+' li.'+thisId).removeClass('hide'); 370 } 371 372 }); 373 374 var tempShow = getUrlParam('surfos', '') === 'yes'; 375 if( window.location.pathname === '/surfos/' ) { 376 if(window.location.hostname === 'surfairwordpress.surfairdev.com') { 377 $('.not-for-you-to-see').removeClass('hide').removeClass('not-visible'); 378 $('.for-you-to-see').addClass('hide'); 379 $('.for-you-to-see').addClass('hide'); 380 } else if (tempShow) { 381 $('.not-for-you-to-see').removeClass('hide').removeClass('not-visible'); 382 $('.for-you-to-see').addClass('hide'); 383 $('.for-you-to-see').addClass('hide'); 384 } 385 } 386 387 $('.snappy-air-mobile-overlay').mouseenter(function() { 388 $('body').addClass('temp-overlay'); 389 setTimeout(function() { 390 $('body').removeClass('temp-overlay'); 391 }, 500); 392 }); 393 394 $('.snappy-air-tech-overlay').mouseenter(function() { 395 $('body').addClass('temp-overlay'); 396 setTimeout(function() { 397 $('body').removeClass('temp-overlay'); 398 }, 500); 399 }); 400 $('.snappy-investors-overlay').mouseenter(function() { 401 $('body').addClass('temp-overlay'); 402 setTimeout(function() { 403 $('body').removeClass('temp-overlay'); 404 }, 500); 405 }); 406 407 $('a.close-module').on('click', function(e) { 408 e.preventDefault(); 409 var modId = $(this).attr('module-id'); 410 $('#'+modId).addClass('hide'); 411 }); 412 413 function toggleButton(playing, id) { 414 let videoElem = document.getElementById(id); 415 if (playing) { 416 $('button[data-id='+id+'].play-btn').removeClass('active'); 417 $('button[data-id='+id+'].pause-btn').addClass('active'); 418 videoElem.play(); 419 //playButton.textContent = "Pause"; 420 //playButton.setAttribute("aria-label", "Pause"); 421 } else { 422 $('button[data-id='+id+'].play-btn').addClass('active'); 423 $('button[data-id='+id+'].pause-btn').removeClass('active'); 424 videoElem.pause() 425 //playButton.textContent = "Play"; 426 //playButton.setAttribute("aria-label", "Play"); 427 } 428 } 429 430 $('button.play-btn').on('click', function(e) { 431 e.preventDefault(); 432 var playId = $(this).attr('data-id'); 433 toggleButton(true, playId); 434 //console.log('playId', playId); 435 }); 436 437 $('button.pause-btn').on('click', function(e) { 438 e.preventDefault(); 439 var playId = $(this).attr('data-id'); 440 toggleButton(false, playId); 441 //console.log('playId', playId); 442 }); 443
444 $('#myModal1').on('hidden.bs.modal', function (e) { 445 var $iframe = $('#vimeoId1'); 446 var vidsrc = $iframe.attr('src'); // Save the current source 447 $iframe.attr('src', ''); // Stop the video 448 $iframe.attr('src', vidsrc); // Reset the source for future use 449 }); 450 $('#myModal2').on('hidden.bs.modal', function (e) { 451 var $iframe = $('#vimeoId2'); 452 var vidsrc = $iframe.attr('src'); // Save the current source 453 $iframe.attr('src', ''); // Stop the video 454 $iframe.attr('src', vidsrc); // Reset the source for future use 455 }); 456 $('#myModal3').on('hidden.bs.modal', function (e) { 457 var $iframe = $('#vimeoId3'); 458 var vidsrc = $iframe.attr('src'); // Save the current source 459 $iframe.attr('src', ''); // Stop the video 460 $iframe.attr('src', vidsrc); // Reset the source for future use 461 }); 462 $('#myModal4').on('hidden.bs.modal', function (e) { 463 var $iframe = $('#vimeoId4'); 464 var vidsrc = $iframe.attr('src'); // Save the current source 465 $iframe.attr('src', ''); // Stop the video 466 $iframe.attr('src', vidsrc); // Reset the source for future use 467 }); 468 469 $('a.module-closex').on('click', function(e) { 470 e.preventDefault(); 471 var sectionId = $(this).attr('data-id'); 472 $('#'+sectionId).addClass('hide'); 473 }); 474 475 //$('.flip-book-wrapper').addClass('loaded'); 476 477 if($('.investor-check-day').length) { 478 var greetingStr = getTimeOfDayGreeting() + ', INVESTOR'; 479 //console.log('investor-check-day',greetingPrefix); 480 $('.investor-check-day').html(greetingStr); 481 } 482 483 $('.list-item-notification-close').on('click', function(e) { 484 e.preventDefault(); 485 var thisId = $(this).attr('data-id'); 486 $('#'+thisId+'.each-notification-wrapper').addClass('hidden'); 487 console.log('thisId', thisId); 488 }); 489 490 491 }); 492})(jQuery); 493 494 495 496 497/* --- Added 2026-08-11: scroll reveal (IntersectionObserver) --- 498 Pairs with the .sa-reveal CSS in Customizer > Additional CSS. 499 Safe by design: only elements marked here ever get hidden, so if 500 this script does not run, nothing on the page is hidden. 501 Revert: restore hb_custom.js.bak from uploads/theme-backups-20260811/ --- */ 502(function () { 503 function saReveal() { 504 try { 505 if (!('IntersectionObserver' in window)) return; 506 if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 507 var rows = Array.prototype.slice.call(document.querySelectorAll('#content .vc_row')); 508 rows = rows.filter(function (r) { return !(r.parentElement && r.parentElement.closest('.vc_row')); }); 509 if (!rows.length) return; 510 var io = new IntersectionObserver(function (entries) { 511 entries.forEach(function (en) { 512 if (en.isIntersecting) { 513 en.target.classList.add('sa-in'); 514 io.unobserve(en.target); 515 } 516 }); 517 }, { rootMargin: '0px 0px -90px 0px', threshold: 0 }); 518 rows.forEach(function (r) { 519 var rect = r.getBoundingClientRect(); 520 if (rect.top < window.innerHeight) { 521 r.classList.add('sa-in', 'sa-instant'); 522 } else { 523 r.classList.add('sa-reveal'); 524 io.observe(r); 525 } 526 }); 527 } catch (e) { /* never break the page */ } 528 } 529 if (document.readyState === 'loading') { 530 document.addEventListener('DOMContentLoaded', saReveal); 531 } else { 532 saReveal(); 533 } 534})(); 535 536 537/* --- Added 2026-08-12: hero carousel autoplay + trackpad swipe (sa-hero-carousel) 538 1. Autoplay every 7s (slick pauses it on hover/focus by itself). 539 2. Two-finger horizontal trackpad scroll moves between slides. 540 Only acts when the gesture is clearly horizontal, so normal 541 vertical page scrolling is never intercepted. preventDefault 542 on those gestures also stops Chrome's swipe-to-go-back. 543 Everything is wrapped in try/catch and only runs once slick has 544 initialised, so a failure here can never block the page. 545 Revert: restore hb_custom.js.bak2-before-carousel from 546 uploads/theme-backups-20260811/ --- */ 547(function () { 548 var AUTOPLAY_MS = 7000; 549 var tries = 0; 550
551 function init() { 552 try { 553 var $ = window.jQuery; 554 if (!$) return; 555 var $hero = $('.homepage-hero-video .listing-wrapper').first(); 556 557 if (!$hero.length || !$hero.hasClass('slick-initialized')) { 558 if (++tries < 30) { setTimeout(init, 200); } 559 return; 560 } 561 562 /* 1. autoplay */ 563 $hero.slick('slickSetOption', 'autoplaySpeed', AUTOPLAY_MS, false); 564 $hero.slick('slickSetOption', 'autoplay', true, false); 565 $hero.slick('slickPlay'); 566 567 /* 2. horizontal trackpad scrolling */ 568 var el = $hero.get(0); 569 if (el.dataset.saWheelBound) return; 570 el.dataset.saWheelBound = '1'; 571 572 /* One gesture = one slide. The axis is decided once at the 573 start of a swipe and held until the gesture ends, because 574 a trackpad emits frames whose individual dx/dy ratio dips 575 below any per-event threshold â judging each frame on its 576 own lets some through and the page nudges vertically 577 mid-swipe, which reads as a wobble. */ 578 var AXIS_END_MS = 160; 579 var axis = null; 580 var moved = false; 581 var endTimer = null; 582 583 el.addEventListener('wheel', function (e) { 584 var dx = e.deltaX; 585 var dy = e.deltaY; 586 587 if (!axis) { 588 /* too small to classify yet - wait for a real frame */ 589 if (Math.abs(dx) < 8 && Math.abs(dy) < 8) { return; } 590 axis = (Math.abs(dx) > Math.abs(dy)) ? 'h' : 'v'; 591 } 592 593 clearTimeout(endTimer); 594 endTimer = setTimeout(function () { 595 axis = null; 596 moved = false; 597 }, AXIS_END_MS); 598 599 /* vertical gesture: hands off, let the page scroll */ 600 if (axis !== 'h') { return; } 601 602 /* horizontal gesture: swallow every frame of it */ 603 e.preventDefault(); 604 605 if (moved) { return; } 606 moved = true; 607 $hero.slick(dx > 0 ? 'slickNext' : 'slickPrev'); 608 }, { passive: false }); 609 } catch (err) { /* never break the page */ } 610 } 611 612 if (document.readyState === 'loading') { 613 document.addEventListener('DOMContentLoaded', function () { setTimeout(init, 300); }); 614 } else { 615 setTimeout(init, 300); 616 } 617})(); 618 619/* ---- Chat widget open chime (v3 - Aug 13, 2026) --------------------------- 620 Light and airy rather than percussive: two quiet sine partials, a gentle 621 25ms swell and a long smooth tail. No noise transient, and the low-pass 622 sits high so it reads as air, not as a knock. 623 -------------------------------------------------------------------------- */ 624(function () { 625 var ctx = null, last = 0; 626 627 function voice(t, freq, peak, attack, release) { 628 var o = ctx.createOscillator(), g = ctx.createGain(); 629 o.type = 'sine'; 630 o.frequency.setValueAtTime(freq, t); 631 o.frequency.exponentialRampToValueAtTime(freq * 0.99, t + release); /* barely-there settle */ 632 g.gain.setValueAtTime(0.0001, t); 633 g.gain.linearRampToValueAtTime(peak, t + attack); 634 g.gain.exponentialRampToValueAtTime(0.0001, t + release); 635 o.connect(g); 636 o.start(t); 637 o.stop(t + release + 0.05); 638 return g; 639 } 640 641 function chime() { 642 var AC = window.AudioContext || window.webkitAudioContext; 643 if (!AC) return; 644 try { 645 if (!ctx) ctx = new AC(); 646 if (ctx.state === 'suspended') ctx.resume(); 647 var t = ctx.currentTime + 0.01; 648 649 var out = ctx.createGain(); 650 out.gain.value = 0.9; 651 var hp = ctx.createBiquadFilter(); 652 hp.type = 'highpass'; hp.frequency.value = 500; /* take the weight out */ 653 var lp = ctx.createBiquadFilter(); 654 lp.type = 'lowpass'; lp.frequency.value = 6500; lp.Q.value = 0.4; 655 out.connect(hp); hp.connect(lp); lp.connect(ctx.destination); 656 657 voice(t, 1174.7, 0.042, 0.025, 0.55).connect(out); /* D6 */ 658 voice(t + 0.03, 1760.0, 0.013, 0.035, 0.38).connect(out); /* A6, a soft shimmer */ 659 } catch (e) { /* audio must never break the click */ } 660 } 661 662 document.addEventListener('pointerdown', function (e) { 663 var b = e.target && e.target.closest && e.target.closest('.surfos-chat-bubble'); 664 if (!b) return; 665 var now = Date.now(); 666 if (now - last < 400) return; 667 last = now; 668 var win = document.querySelector('.surfos-chat-window'); 669 if (win && getComputedStyle(win).display !== 'none') return; /* only on open */ 670 chime(); 671 }, true); 672})(); 673 674/* ---- live "Aligning the Ecosystem" background (homepage + /future/) ------- 675 Replaces the flat platformslide JPEG with the SurfOS animated gradient + 676 grain, vector concentric rings, and three aircraft types orbiting the rings. 677 678 Ring geometry is measured from the original artwork by radial-profiling the 679 JPEG: centre (1417, 444) in a 2000x864 frame, radii 45 / 109 / 187 / 272 /
680 380 / 630 / 945. The SVG keeps that exact viewBox and uses 681 preserveAspectRatio="xMidYMid slice" so it crops like background-size: cover. 682 683 NO-COLLISION GUARANTEE - two independent rules, both structural: 684 1. Aircraft sharing a ring have the same duration and direction, so their 685 angular separation is fixed forever (evenly spaced => 2r*sin(pi/n) apart). 686 2. Aircraft on different rings are separated by at least the radial gap. 687 Only rings 187 / 380 / 630 carry traffic, so the smallest gap is 193 688 units; the largest icon sits inside a 40-unit box, so its bounding 689 radius is at most 28, leaving ~136 units of clearance at the very worst. 690 Neither depends on phase, so there is no moment in the cycle where they meet. 691 Twelve aircraft, evenly spaced on each ring. Measured over the full 4800s 692 cycle (8000 samples): see the project doc for the current figures. Zero 693 overlapping frames. 694 -------------------------------------------------------------------------- */ 695(function () { 696 var CX = 1417, CY = 444; 697 var RINGS = [45, 109, 187, 272, 380, 630, 945]; 698 699 /* one entry per orbit; `at` are the angular positions (deg) on that ring. 700 Only every other ring carries traffic - that is what buys the 193-unit 701 minimum radial gap quoted above. */ 702 var ORBITS = [ 703 { r: 187, dur: 200, at: [15, 135, 255], type: ['caravan', 'evtol', 'beta'] }, 704 { r: 380, dur: 320, at: [105, 225, 345], type: ['jet', 'beta', 'caravan'] }, 705 { r: 630, dur: 480, at: [40, 100, 160, 220, 280, 340], type: ['caravan', 'beta', 'jet', 'beta', 'caravan', 'beta'] } 706 ]; 707 708 /* Icons come straight from the Media Library â the same four the other 709 animated maps use. Each SVG is drawn top-down with the nose pointing UP, 710 white, so they are referenced with <image> rather than inlined: no id or 711 class collisions (two of them carry .cls-1 / .st0 style blocks and a 712 clipPath called "clippath"), and no 15KB of markup in this file. 713 Motion is clockwise, so at the 3-o'clock anchor the heading is +Y â which 714 for a nose-up icon means rotate(180). */ 715 var BASE = '/wp-content/uploads/2026/08/'; 716 var ICON_SIZE = 40; /* longest edge, in viewBox units */ 717 var ICONS = { 718 beta: { f: 'BETA-AIRCRAFT.svg', w: 723.52, h: 555.76 }, 719 caravan: { f: 'caravan.svg', w: 441.4, h: 360.2 }, 720 evtol: { f: 'genericEVTOL.svg', w: 162, h: 137 }, 721 jet: { f: 'generic_private_jet.svg', w: 244, h: 253 } 722 }; 723 724 function icon(key) { 725 var m = ICONS[key]; 726 var k = ICON_SIZE / Math.max(m.w, m.h), w = m.w * k, h = m.h * k; 727 return '<image href="' + BASE + m.f + '" x="' + (-w / 2).toFixed(2) + 728 '" y="' + (-h / 2).toFixed(2) + '" width="' + w.toFixed(2) + 729 '" height="' + h.toFixed(2) + '" preserveAspectRatio="xMidYMid meet"/>'; 730 } 731 732 function build() { 733 var rings = RINGS.map(function (r, i) { 734 /* the outermost ring is noticeably fainter in the original */ 735 var o = i === RINGS.length - 1 ? 0.09 : (i === 0 ? 0.26 : 0.22); 736 return '<circle cx="' + CX + '" cy="' + CY + '" r="' + r + 737 '" fill="none" stroke="rgba(255,255,255,' + o + ')" stroke-width="1.2"/>'; 738 }).join(''); 739 740 var craft = ''; 741 ORBITS.forEach(function (o) { 742 o.at.forEach(function (deg, k) { 743 /* phase is a negative delay rather than a per-element keyframe, so every 744 orbiter can share one 0->360 animation */ 745 var delay = -(deg / 360) * o.dur; 746 craft += 747 '<g class="saf-eco-orb" style="animation-duration:' + o.dur + 's;' + 748 'animation-delay:' + delay.toFixed(2) + 's">' + 749 '<g transform="translate(' + (CX + o.r) + ',' + CY + ') rotate(180)">' + icon(o.type[k]) + 750 '</g></g>'; 751 }); 752 }); 753 754 return '' + 755 '<div class="saf-eco-bg">' + 756 '<gradient-bg speed="6" pointer="1.6"></gradient-bg>' + 757 '<div class="saf-eco-tint"></div>' + 758 '<svg class="saf-eco-art" viewBox="0 0 2000 864" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false">' + 759 '<g class="saf-eco-rings">' + rings + '</g>' + 760 '<g class="saf-eco-traffic">' + craft + '</g>' + 761 '</svg>' + 762 '</div>'; 763 } 764 765 function ensureGradient() { 766 if (window.customElements && customElements.get('gradient-bg')) return; 767 if (document.querySelector('script[src*="gradient-bg.js"]')) return; 768 var s = document.createElement('script'); 769 s.src = '/wp-content/uploads/gradient-bg.js'; 770 s.defer = true; 771 document.head.appendChild(s); 772 } 773
774 function init() { 775 /* The same module appears on the homepage and on /future/ â identical block, 776 identical artwork â so it is found by content, not by page, and every 777 match on the page gets its own layer. */ 778 var targets = [].filter.call( 779 document.querySelectorAll('.hbg-block-content.page-hero.has-background-image'), 780 function (b) { return /ALIGNING THE ECOSYSTEM/i.test(b.textContent || ''); }); 781 /* the two-up cards ship their own gradient-bg elements in the page HTML, 782 so the script has to load even when no eco module is present */ 783 if (!targets.length && !document.querySelector('gradient-bg')) return; 784 785 ensureGradient(); 786 if (!targets.length) return; 787 targets.forEach(function (target) { 788 var wrap = target.querySelector('.module-wrapper'); 789 if (!wrap || wrap.querySelector('.saf-eco-bg')) return; 790 wrap.classList.add('saf-eco-host'); 791 wrap.insertAdjacentHTML('afterbegin', build()); 792 /* On the homepage the whole card links through to /future/. Skipped on 793 /future/ itself so the module never links to the page it is on. */ 794 if (location.pathname.indexOf('/future') !== 0) { 795 var a = document.createElement('a'); 796 a.className = 'saf-eco-link'; 797 a.href = '/future/'; 798 a.setAttribute('aria-label', 'Aligning the ecosystem: a technology platform'); 799 wrap.appendChild(a); 800 } 801 requestAnimationFrame(function () { 802 var bg = wrap.querySelector('.saf-eco-bg'); 803 if (bg) bg.classList.add('saf-eco-in'); 804 }); 805 }); 806 } 807 808 if (document.readyState === 'loading') { 809 document.addEventListener('DOMContentLoaded', init); 810 } else { 811 init(); 812 } 813})(); 814/* ---- /future/ â staggered module entrance --------------------------------- 815 The theme reveals whole rows (.sa-reveal), but the big row that holds the 816 hero, the four cards and the ecosystem module is marked sa-instant, so the 817 entire top of this page had no entrance at all. This reveals each module 818 individually instead, and the row-level reveal is switched off on this page 819 in the CSS so there is only ever one animation layer. 820 821 IMPORTANT: the class goes on the module's .module-wrapper, NOT on the 822 .hbg-block-content block. The block is what carries the dark blue background 823 in the upper half of the page â fading the block fades the background with 824 it and the light grey body flashes through. The wrapper is the visible card, 825 so animating that moves what you see and leaves the backdrop alone. 826 -------------------------------------------------------------------------- */ 827(function () { 828 function init() { 829 if (!document.body || document.body.className.indexOf('page-id-461') < 0) return; 830 831 var targets = [].map.call( 832 document.querySelectorAll('#content .hbg-block-content'), 833 function (b) { return b.querySelector(':scope > .module-wrapper'); } 834 ).filter(Boolean); 835 /* the two-up cards are raw HTML, not hbg blocks, so they are collected 836 separately - they reveal on the same terms as everything else */ 837 targets = targets.concat( [].slice.call( document.querySelectorAll('#content .saf-twoup-card') ) ); 838 if (!targets.length) return; 839 840 var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 841 targets.forEach(function (t) { t.classList.add('saf-rise'); }); 842 if (reduce || !('IntersectionObserver' in window)) { 843 targets.forEach(function (t) { t.classList.add('saf-rise-in'); }); 844 return; 845 } 846 847 var io = new IntersectionObserver(function (entries) { 848 entries.filter(function (e) { return e.isIntersecting; }) 849 .forEach(function (e, i) { 850 /* small cascade when several land together, capped so a long run 851 never feels like it is queueing */ 852 setTimeout(function () { e.target.classList.add('saf-rise-in'); }, Math.min(i, 3) * 100); 853 io.unobserve(e.target); 854 }); 855 }, { rootMargin: '0px 0px -10% 0px', threshold: 0.01 }); 856 857 /* Nothing is revealed until the real faces are ready, so a module can never 858 appear in the fallback serif. future-preload.php sets window.safFontsReady 859 and fires saf:fonts-ready; the timeout is the backstop if it never does. */ 860 var started = false;
861 function start() { 862 if (started) { return; } 863 started = true; 864 targets.forEach(function (t) { io.observe(t); }); 865 } 866 if (window.safFontsReady) { 867 start(); 868 } else { 869 document.addEventListener('saf:fonts-ready', start, { once: true }); 870 setTimeout(start, 3000); 871 } 872 873 /* safety net â if the observer never fires (throttled tab, odd browser), 874 nothing is left invisible */ 875 setTimeout(function () { 876 targets.forEach(function (t) { t.classList.add('saf-rise-in'); }); 877 }, 2500); 878 } 879 880 if (document.readyState === 'loading') { 881 document.addEventListener('DOMContentLoaded', init); 882 } else { 883 init(); 884 } 885})(); 886 887 888/* ---- homepage stat counters ------------------------------------------------ 889 Counts the big figures up as they scroll into view: the ~5,000 / ~90% / ~2.4b 890 trio and the 60,000+ / 800+ map rows. 891 892 Values are PARSED OUT OF THE RENDERED TEXT, so the markup stays the single 893 source of truth and editing a number in WordPress needs no code change. The 894 prefix ("~"), the suffix ("%", "b", "+") and the thousands grouping are kept, 895 and the ORIGINAL string is written back on the final frame rather than a 896 reconstruction - so "~2.4b" ends exactly as authored, whatever the maths did. 897 898 Degrades to the authored number if anything is missing: no IntersectionObserver, 899 reduced motion, unparseable text, or JS off entirely. 900 -------------------------------------------------------------------------- */ 901(function () { 902 var SEL = ".homepage-route-data span.title, .listing-wrapper .glisting-item .item-info h2.title, .saf-statrow .saf-stat-value"; 903 var DUR = 1600; 904 905 function parse(txt) { 906 var m = /^([^0-9]*)([0-9][0-9.,]*)(.*)$/.exec(String(txt).trim()); 907 if (!m) { return null; } 908 var numStr = m[2].replace(/[.,]+$/, ""); 909 var plain = numStr.replace(/,/g, ""); 910 var value = parseFloat(plain); 911 if (!isFinite(value)) { return null; } 912 var dot = plain.indexOf("."); 913 return { 914 prefix: m[1], 915 suffix: m[3], 916 value: value, 917 decimals: dot < 0 ? 0 : plain.length - dot - 1, 918 grouped: numStr.indexOf(",") >= 0, 919 original: String(txt) 920 }; 921 } 922 923 function render(el, info, v) { 924 var s = info.decimals ? v.toFixed(info.decimals) : String(Math.round(v)); 925 if (info.grouped) { 926 var parts = s.split("."); 927 parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); 928 s = parts.join("."); 929 } 930 el.textContent = info.prefix + s + info.suffix; 931 } 932 933 function run(el, info) { 934 var start = null; 935 el.classList.add("saf-stat-counting"); 936 (function step(ts) { 937 if (start === null) { start = ts; } 938 var p = Math.min(1, (ts - start) / DUR); 939 var eased = 1 - Math.pow(1 - p, 3); 940 if (p < 1) { 941 render(el, info, info.value * eased); 942 requestAnimationFrame(step); 943 } else { 944 el.textContent = info.original; 945 el.classList.remove("saf-stat-counting"); 946 } 947 })(performance.now()); 948 } 949 950 function init() { 951 /* homepage, plus the copy of the same stat row under the map on /future/ */ 952 var cls = document.body ? document.body.className : ""; 953 if (cls.indexOf("home") < 0 && cls.indexOf("page-id-461") < 0) { return; } 954 var els = [].slice.call(document.querySelectorAll(SEL)).filter(function (e) { 955 return /[0-9]/.test(e.textContent) && parse(e.textContent); 956 }); 957 if (!els.length) { return; } 958 if (!("IntersectionObserver" in window)) { return; } 959 if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) { return; } 960 961 var io = new IntersectionObserver(function (entries) { 962 entries.forEach(function (en) { 963 if (!en.isIntersecting) { return; } 964 io.unobserve(en.target); 965 if (en.target.safStatCount) { run(en.target, en.target.safStatCount); } 966 }); 967 }, { threshold: 0.4 }); 968 969 els.forEach(function (e) { 970 var info = parse(e.textContent); 971 e.safStatCount = info; 972 /* Tag the stat and its caps label so the CSS can find them. This has to be 973 done from here: the theme reuses .item-note 38 times across the homepage 974 and only these five belong to a stat - there is no class or ancestor that 975 separates them, only the fact that the heading parses as a number, which 976 is exactly what this script has just established. */ 977 e.classList.add("saf-stat"); 978 var host = e.closest ? e.closest("li") : null; 979 if (host) { 980 [].forEach.call(host.querySelectorAll(".item-note, .sub-title, .note"), function (l) { 981 l.classList.add("saf-stat-note"); 982 }); 983 } 984 /* park at zero straight away - these sections sit below the fold and behind 985 the theme reveal, so the reset is never seen, and it avoids the number 986 snapping back down the moment the observer fires */ 987 render(e, info, 0); 988 io.observe(e); 989 }); 990 } 991 992 if (document.readyState === "loading") { 993 document.addEventListener("DOMContentLoaded", init); 994 } else { init(); } 995})();
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.