PageSourceSearch

https://www.surfair.com/wp-content/themes/surfair/assets/js/hb_custom.js?ver=fcfb53019a9cd5de232d25bdb5172b44

js surfair.com collected 2026-09-25 02:37:13 UTC 38,150 bytes, 995 lines download raw bytes

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.