PageSourceSearch

https://talk-type.com/wp-content/themes/talktype-2026/assets/js/pages/home.js?ver=1789738511

js talk-type.com collected 2026-09-24 12:39:12 UTC 52,434 bytes, 1,875 lines download raw bytes

1document.addEventListener('DOMContentLoaded', function () {
2	const hero = document.querySelector('.js-tt-home-hero');
3	const heroVisual = hero
4		? hero.querySelector('.tt-home-hero__visual-wrap')
5		: null;
6	const textFillSections = document.querySelectorAll('.js-tt-text-fill');
7
8	const benefitsViewport = document.querySelector('.js-key-benefits-viewport');
9	const benefitsTrack = benefitsViewport
10		? benefitsViewport.querySelector('.tt-home-key-benefits__track')
11		: null;
12	const prevButton = document.querySelector('.js-key-benefits-prev');
13	const nextButton = document.querySelector('.js-key-benefits-next');
14
15	const useCasesViewport = document.querySelector('.js-use-cases-viewport');
16	const useCasesTrack = useCasesViewport
17		? useCasesViewport.querySelector('.tt-home-use-cases__track')
18		: null;
19	const useCasesPrevButton = document.querySelector('.js-use-cases-prev');
20	const useCasesNextButton = document.querySelector('.js-use-cases-next');
21
22	const featureStackSection = document.querySelector('.js-tt-feature-stack');
23	const featureScrollArea = featureStackSection
24		? featureStackSection.querySelector('.tt-home-features__scroll')
25		: null;
26	const featureSticky = featureStackSection
27		? featureStackSection.querySelector('.tt-home-features__sticky')
28		: null;
29	const featureStack = featureStackSection
30		? featureStackSection.querySelector('.tt-home-features__stack')
31		: null;
32	const featureCards = featureStackSection
33		? Array.from(featureStackSection.querySelectorAll('.js-tt-feature-card'))
34		: [];
35
36	const userStoriesCarousel = document.querySelector('.js-user-stories-carousel');
37	const userStoriesTrack = document.querySelector('.js-user-stories-track');
38
39	let animTicking = false;
40	let cachedFeatureStackSettings = null;
41	let cachedHeroAnimationDistance = 1;
42	let cachedViewportHeight = 0;
43	let cachedHeroPreScrollWidth = 1260;
44	let cachedHeroPreScrollHeight = 680;
45	let cachedHeroSettledWidth = 1260;
46	let cachedHeroSettledHeight = 680;
47	let cachedFeatureAvailableScroll = 1;
48	let activeFeatureCardIndex = 0;
49	let featureStackStatus = null;
50
51	function clamp(value, min, max) {
52		return Math.max(min, Math.min(max, value));
53	}
54
55	function easeInOutCubic(t) {
56		return t < 0.5
57			? 4 * t * t * t
58			: 1 - Math.pow(-2 * t + 2, 3) / 2;
59	}
60
61	const carouselDragClickThreshold = 6;
62	const carouselReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
63
64	const getStartedSections = document.querySelectorAll('.js-tt-get-started-video');
65
66	if (getStartedSections.length) {
67		getStartedSections.forEach(function (section) {
68			const video = section.querySelector('.js-tt-get-started-video-player');
69			const steps = Array.from(section.querySelectorAll('.js-tt-get-started-step'));
70
71			if (!video || !steps.length) {
72				return;
73			}
74
75			let progressFrameId = null;
76			let sectionIsVisible = false;
77			let currentActiveStepIndex = null;
78			let stepSwitchTimeout = null;
79			const stepSwitchDuration = 220;
80			const stepsContainer = section.querySelector('.tt-home-get-started__steps');
81
82
83			video.muted = true;
84			video.loop = true;
85			video.setAttribute('muted', '');
86			video.setAttribute('loop', '');
87			video.setAttribute('playsinline', '');
88
89			function getStepMarker(step) {
90				const marker = parseFloat(step.getAttribute('data-video-marker'));
91
92				if (Number.isNaN(marker)) {
93					return 0;
94				}
95
96				return marker;
97			}
98
99			function getVideoDuration() {
100				return Number.isFinite(video.duration) && video.duration > 0
101					? video.duration
102					: 0;
103			}
104
105			function markerToRatio(marker) {
106				const duration = getVideoDuration();
107
108				if (marker <= 1) {
109					return clamp(marker, 0, 1);
110				}
111
112				if (!duration) {
113					return 0;
114				}
115
116				return clamp(marker / duration, 0, 1);
117			}
118
119			function markerToTime(marker) {
120				const duration = getVideoDuration();
121
122				if (marker <= 1) {
123					return duration ? marker * duration : 0;
124				}
125
126				return marker;
127			}
128
129			function getStepRatios() {
130				return steps.map(function (step) {
131					return markerToRatio(getStepMarker(step));
132				});
133			}
134
135			function getCurrentRatio() {
136				const duration = getVideoDuration();
137
138				if (!duration) {
139					return 0;
140				}
141
142				return clamp(video.currentTime / duration, 0, 1);
143			}
144
145			function getActiveStepIndex(currentRatio, stepRatios) {
146				let activeIndex = 0;
147
148				stepRatios.forEach(function (ratio, index) {
149					if (currentRatio >= ratio - 0.001) {
150						activeIndex = index;
151					}
152				});
153
154				return activeIndex;
155			}
156
157function markStepSwitch(nextActiveIndex) {
158	if (currentActiveStepIndex === null) {
159		currentActiveStepIndex = nextActiveIndex;
160		return;
161	}
162
163	if (currentActiveStepIndex === nextActiveIndex) {
164		return;
165	}
166
167	currentActiveStepIndex = nextActiveIndex;
168
169	if (!stepsContainer) {
170		return;
171	}
172
173	stepsContainer.classList.add('is-switching');
174
175	if (stepSwitchTimeout) {
176		window.clearTimeout(stepSwitchTimeout);
177	}
178
179	stepSwitchTimeout = window.setTimeout(function () {
180		stepsContainer.classList.remove('is-switching');
181		updateGetStartedSteps();
182	}, stepSwitchDuration);
183}
184
185function updateGetStartedSteps() {
186	const currentRatio = getCurrentRatio();
187	const stepRatios = getStepRatios();
188	const activeIndex = getActiveStepIndex(currentRatio, stepRatios);
189
190	markStepSwitch(activeIndex);
191
192	steps.forEach(function (step, index) {
193					const startRatio = stepRatios[index] || 0;
194					const endRatio = stepRatios[index + 1] !== undefined
195						? stepRatios[index + 1]
196						: 1;
197
198					let stepProgress = 0;
199
200					if (index < activeIndex) {
201						stepProgress = 1;
202					} else if (index === activeIndex) {
203						const segmentLength = Math.max(0.001, endRatio - startRatio);
204						stepProgress = clamp((currentRatio - startRatio) / segmentLength, 0, 1);
205					}
206
207					step.style.setProperty('--step-progress', stepProgress.toFixed(4));
208					step.classList.toggle('tt-home-get-started__step--active', index === activeIndex);
209					step.classList.toggle('is-complete', index < activeIndex);
210
211					if (index === activeIndex) {
212						step.setAttribute('aria-current', 'step');
213					} else {
214						step.removeAttribute('aria-current');
215					}
216				});
217			}
218
219			function stopProgressLoop() {
220				if (progressFrameId) {
221					window.cancelAnimationFrame(progressFrameId);
222					progressFrameId = null;
223				}
224			}
225
226			function startProgressLoop() {
227				stopProgressLoop();
228
229				function tick() {
230					updateGetStartedSteps();
231
232					if (!video.paused && !video.ended) {
233						progressFrameId = window.requestAnimationFrame(tick);
234					}
235				}
236
237				progressFrameId = window.requestAnimationFrame(tick);
238			}
239
240			function playGetStartedVideo() {
241				const playPromise = video.play();
242
243				if (playPromise && typeof playPromise.catch === 'function') {
244					playPromise.catch(function () {
245						updateGetStartedSteps();
246					});
247				}
248			}
249
250			function pauseGetStartedVideo() {
251				video.pause();
252				updateGetStartedSteps();
253			}
254
255			function seekToStep(stepIndex) {
256				const step = steps[stepIndex];
257
258				if (!step) {
259					return;
260				}
261
262				const duration = getVideoDuration();
263
264				if (!duration) {
265					video.addEventListener('loadedmetadata', function handleLoadedMetadata() {
266						seekToStep(stepIndex);
267					}, { once: true });
268
269					video.load();
270					return;
271				}
272
273				const marker = getStepMarker(step);
274				const targetTime = clamp(markerToTime(marker), 0, duration);
275
276				video.currentTime = targetTime;
277				updateGetStartedSteps();
278				playGetStartedVideo();
279			}
280
281			function isSectionInView() {
282				const rect = section.getBoundingClientRect();
283				const windowHeight = window.innerHeight || document.documentElement.clientHeight;
284				const visibleAmount = Math.min(rect.bottom, windowHeight) - Math.max(rect.top, 0);
285				const visibleRatio = visibleAmount / Math.max(1, rect.height);
286
287				return visibleRatio >= 0.35;
288			}
289
290			function updateSectionPlayback() {
291				const isVisible = isSectionInView();
292
293				if (isVisible === sectionIsVisible) {
294					return;
295				}
296
297				sectionIsVisible = isVisible;
298
299				if (sectionIsVisible) {
300					playGetStartedVideo();
301				} else {
302					pauseGetStartedVideo();
303				}
304			}
305
306			steps.forEach(function (step, index) {
307				step.addEventListener('click', function () {
308					seekToStep(index);
309				});
310			});
311
312			video.addEventListener('loadedmetadata', updateGetStartedSteps);
313			video.addEventListener('durationchange', updateGetStartedSteps);
314			video.addEventListener('timeupdate', updateGetStartedSteps);
315			video.addEventListener('seeking', updateGetStartedSteps);
316			video.addEventListener('seeked', updateGetStartedSteps);
317			video.addEventListener('play', startProgressLoop);
318			video.addEventListener('pause', function () {
319				stopProgressLoop();
320				updateGetStartedSteps();
321			});
322			video.addEventListener('ended', function () {
323				stopProgressLoop();
324				updateGetStartedSteps();
325			});
326
327			if ('IntersectionObserver' in window) {
328				const sectionObserver = new IntersectionObserver(function (entries) {
329					entries.forEach(function (entry) {
330						sectionIsVisible = entry.isIntersecting;
331
332						if (sectionIsVisible) {
333							playGetStartedVideo();
334						} else {
335							pauseGetStartedVideo();
336						}
337					});
338				}, {
339					threshold: 0.35
340				});
341
342				sectionObserver.observe(section);
343			} else {
344				window.addEventListener('scroll', updateSectionPlayback, { passive: true });
345				window.addEventListener('resize', updateSectionPlayback);
346				updateSectionPlayback();
347			}
348
349			updateGetStartedSteps();
350		});
351	}
352
353	function updateScrollAnimations() {
354		// Phase 1: do layout reads before DOM writes
355
356		const isMobileHero = hero && window.matchMedia('(max-width: 959px)').matches;
357		const heroRect = hero ? hero.getBoundingClientRect() : null;
358
359		const textFillReads = [];
360		textFillSections.forEach(function (section) {
361			const textBlock = section.querySelector('.tt-home-text-fill__text');
362			const words = section.querySelectorAll('.js-tt-text-fill-word');
363			if (!textBlock || !words.length) { return; }
364			textFillReads.push({ words: words, rect: textBlock.getBoundingClientRect() });
365		});
366
367		const featureRect = (featureScrollArea && shouldUseFeatureStack())
368			? featureScrollArea.getBoundingClientRect()
369			: null;
370
371		// Phase 2: apply DOM writes after layout reads
372
373		if (hero) {
374			if (isMobileHero) {
375				hero.style.setProperty('--hero-progress', '0');
376				hero.style.setProperty('--hero-settle-progress', '1');
377				hero.style.setProperty('--hero-visual-width', cachedHeroSettledWidth.toFixed(2) + 'px');
378				hero.style.setProperty('--hero-visual-height', cachedHeroSettledHeight.toFixed(2) + 'px');
379				hero.style.setProperty('--hero-video-blur', '0px');
380				hero.style.setProperty('--hero-video-scale', '1');
381			} else if (heroRect) {
382				const progress = clamp((-heroRect.top) / cachedHeroAnimationDistance, 0, 1);
383				const settleProgress = progress;
384				const visualWidth = cachedHeroPreScrollWidth
385					+ ((cachedHeroSettledWidth - cachedHeroPreScrollWidth) * settleProgress);
386				const visualHeight = cachedHeroPreScrollHeight
387					+ ((cachedHeroSettledHeight - cachedHeroPreScrollHeight) * settleProgress);
388				const blurAmount = 16 * (1 - settleProgress);
389				const videoScale = 1 + (0.025 * (1 - settleProgress));
390				hero.style.setProperty('--hero-progress', progress.toFixed(4));
391				hero.style.setProperty('--hero-settle-progress', settleProgress.toFixed(4));
392				hero.style.setProperty('--hero-visual-width', visualWidth.toFixed(2) + 'px');
393				hero.style.setProperty('--hero-visual-height', visualHeight.toFixed(2) + 'px');
394				hero.style.setProperty('--hero-video-blur', blurAmount.toFixed(2) + 'px');
395				hero.style.setProperty('--hero-video-scale', videoScale.toFixed(4));
396			}
397		}
398
399		const tfStart = cachedViewportHeight * 0.88;
400		const tfEnd = cachedViewportHeight * 0.22;
401		textFillReads.forEach(function (data) {
402			const words = data.words;
403			let progress = (tfStart - data.rect.top) / (tfStart - tfEnd);
404			progress = Math.max(0, Math.min(1, progress));
405			const filledWords = Math.floor(progress * words.length);
406			words.forEach(function (word, index) {
407				const shouldFill = index < filledWords;
408				const shouldNext = index === filledWords && filledWords < words.length;
409				if (shouldFill !== word.classList.contains('is-filled')) {
410					word.classList.toggle('is-filled', shouldFill);
411				}
412				if (shouldNext !== word.classList.contains('is-next')) {
413					word.classList.toggle('is-next', shouldNext);
414				}
415			});
416		});
417
418		if (featureRect && featureCards.length) {
419			const settings = getFeatureStackSettings();
420			const totalCompression = settings.totalCompression;
421			const overallProgress = clamp((-featureRect.top) / cachedFeatureAvailableScroll, 0, 1);
422			const currentCompression = totalCompression * overallProgress;
423
424			updateFeatureStackAccessibility(
425				getFeatureActiveCardIndex(currentCompression, settings.compressionStops),
426				document.activeElement === featureStack
427			);
428
429			featureCards.forEach(function (card, index) {
430				const naturalY = settings.naturalPositions[index];
431				const compressedY = settings.compressedPositions[index];
432				const translateY = Math.max(compressedY, naturalY - currentCompression);
433				const depthFromTop = getFeatureCardDepth(index, currentCompression, settings.compressionStops);
434				const scale = 1 - Math.min(depthFromTop * 0.015, 0.06);
435				let shadowOpacity = 0;
436				if (index === 0) {
437					shadowOpacity = 0.10 * (1 - overallProgress);
438				} else {
439					const startCompress = settings.compressionStops[index - 1];
440					const endCompress = settings.compressionStops[index];
441					const localProgress = clamp(
442						(currentCompression - startCompress) / Math.max(1, endCompress - startCompress),
443						0,
444						1
445					);
446					shadowOpacity = 0.16 * (1 - localProgress);
447				}
448				card.style.transform = 'translate3d(0, ' + translateY.toFixed(2) + 'px, 0) scale(' + scale + ')';
449				card.style.boxShadow = '0 16px 40px rgba(24, 35, 47, ' + shadowOpacity.toFixed(4) + ')';
450			});
451		}
452
453		animTicking = false;
454	}
455
456	function requestAnimTick() {
457		if (!animTicking) {
458			window.requestAnimationFrame(updateScrollAnimations);
459			animTicking = true;
460		}
461	}
462
463	if (benefitsViewport && benefitsTrack && prevButton && nextButton) {
464		const benefitsCards = Array.from(
465			benefitsTrack.querySelectorAll('.tt-home-key-benefits-card')
466		);
467
468		let currentBenefitsIndex = 0;
469		let currentTranslate = 0;
470		let benefitsAnimating = false;
471		let dragStartX = 0;
472		let dragStartTranslate = 0;
473		let touchStartX = 0;
474		let touchStartY = 0;
475		let touchStartTranslate = 0;
476		let isTouchDragging = false;
477		let isDragging = false;
478		let suppressBenefitsClick = false;
479
480		function clampBenefitsIndex(index) {
481			return Math.max(0, Math.min(index, benefitsCards.length - 1));
482		}
483
484		function getSnapPoints() {
485			return benefitsCards.map(function (card) {
486				return Math.round(card.offsetLeft);
487			});
488		}
489
490		function getMaxTranslate() {
491			return Math.max(0, benefitsTrack.scrollWidth - benefitsViewport.clientWidth);
492		}
493
494		function setTrackTranslate(value) {
495			const maxTranslate = getMaxTranslate();
496			currentTranslate = Math.max(0, Math.min(value, maxTranslate));
497			benefitsTrack.style.transform = 'translate3d(' + (-currentTranslate) + 'px, 0, 0)';
498		}
499
500		function updateBenefitsButtons() {
501			prevButton.disabled = currentBenefitsIndex <= 0;
502			nextButton.disabled = currentBenefitsIndex >= benefitsCards.length - 1;
503		}
504
505		function animateToSnapIndex(index) {
506			const snapPoints = getSnapPoints();
507			const safeIndex = clampBenefitsIndex(index);
508			const targetTranslate = snapPoints[safeIndex];
509			const startTranslate = currentTranslate;
510			const distance = targetTranslate - startTranslate;
511			const duration = carouselReducedMotion.matches ? 0 : 520;
512
513			benefitsMomentum.cancel();
514
515			if (benefitsAnimating) {
516				return;
517			}
518
519			currentBenefitsIndex = safeIndex;
520			updateBenefitsButtons();
521
522			if (Math.abs(distance) < 1 || duration === 0) {
523				setTrackTranslate(targetTranslate);
524				return;
525			}
526
527			benefitsAnimating = true;
528			const startTime = performance.now();
529
530			function step(now) {
531				const elapsed = now - startTime;
532				const progress = Math.min(elapsed / duration, 1);
533				const eased = easeInOutCubic(progress);
534
535				setTrackTranslate(startTranslate + (distance * eased));
536
537				if (progress < 1) {
538					requestAnimationFrame(step);
539				} else {
540					setTrackTranslate(targetTranslate);
541					benefitsAnimating = false;
542				}
543			}
544
545			requestAnimationFrame(step);
546		}
547
548		function getNearestSnapIndex() {
549			const snapPoints = getSnapPoints();
550			const maxTranslate = getMaxTranslate();
551
552			if (currentTranslate <= 1) {
553				return 0;
554			}
555
556			if (currentTranslate >= maxTranslate - 1) {
557				return benefitsCards.length - 1;
558			}
559
560			let nearestIndex = 0;
561			let nearestDistance = Infinity;
562
563			snapPoints.forEach(function (point, index) {
564				const distance = Math.abs(point - currentTranslate);
565
566				if (distance < nearestDistance) {
567					nearestDistance = distance;
568					nearestIndex = index;
569				}
570			});
571
572			return nearestIndex;
573		}
574
575		function syncBenefitsState() {
576			currentBenefitsIndex = clampBenefitsIndex(getNearestSnapIndex());
577			updateBenefitsButtons();
578		}
579
580		const benefitsMomentum = window.TTCarouselMomentum.create({
581			getMin: function () {
582				return 0;
583			},
584			getMax: getMaxTranslate,
585			setPosition: setTrackTranslate,
586			onSettle: syncBenefitsState
587		});
588
589		prevButton.addEventListener('click', function () {
590			if (benefitsAnimating) {
591				return;
592			}
593			benefitsMomentum.cancel();
594			syncBenefitsState();
595			animateToSnapIndex(currentBenefitsIndex - 1);
596		});
597
598		nextButton.addEventListener('click', function () {
599			if (benefitsAnimating) {
600				return;
601			}
602			benefitsMomentum.cancel();
603			syncBenefitsState();
604			animateToSnapIndex(currentBenefitsIndex + 1);
605		});
606
607		benefitsViewport.setAttribute('tabindex', '0');
608		benefitsViewport.setAttribute('aria-label', 'Key benefits carousel');
609		benefitsViewport.addEventListener('keydown', function (event) {
610			if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
611				return;
612			}
613
614			event.preventDefault();
615			benefitsMomentum.cancel();
616			syncBenefitsState();
617			animateToSnapIndex(currentBenefitsIndex + (event.key === 'ArrowRight' ? 1 : -1));
618		});
619
620		benefitsViewport.addEventListener('pointerdown', function (event) {
621			if (benefitsAnimating || event.pointerType === 'touch') {
622				return;
623			}
624
625			isDragging = true;
626			benefitsMomentum.cancel();
627			dragStartX = event.clientX;
628			dragStartTranslate = currentTranslate;
629			benefitsMomentum.begin(currentTranslate);
630			suppressBenefitsClick = false;
631			benefitsViewport.style.cursor = 'grabbing';
632		});
633
634		let wheelStateTimer;
635		function onWheel(event) {
636			const horizontal = Math.abs(event.deltaX);
637			const vertical = Math.abs(event.deltaY);
638
639			if (horizontal <= vertical) {
640				return; // mostly vertical, allow page scroll
641			}
642
643			event.preventDefault();
644
645			benefitsMomentum.cancel();
646			setTrackTranslate(currentTranslate + event.deltaX);
647
648			clearTimeout(wheelStateTimer);
649
650			wheelStateTimer = setTimeout(() => {
651				syncBenefitsState();
652			}, 120);
653		}
654
655		benefitsViewport.addEventListener(
656			'wheel',
657			onWheel,
658			{ passive: false }
659		);
660
661		window.addEventListener('pointermove', function (event) {
662			if (!isDragging || benefitsAnimating) {
663				return;
664			}
665
666			const deltaX = event.clientX - dragStartX;
667
668			if (Math.abs(deltaX) > carouselDragClickThreshold) {
669				suppressBenefitsClick = true;
670			}
671
672			setTrackTranslate(dragStartTranslate - deltaX);
673			benefitsMomentum.record(currentTranslate);
674		});
675
676		window.addEventListener('pointerup', function () {
677			if (!isDragging) {
678				return;
679			}
680
681			isDragging = false;
682			benefitsViewport.style.cursor = '';
683
684			if (suppressBenefitsClick) {
685				benefitsMomentum.release(currentTranslate);
686			} else {
687				benefitsMomentum.cancel();
688				syncBenefitsState();
689			}
690		});
691
692		benefitsViewport.addEventListener('touchstart', function (event) {
693			if (benefitsAnimating || !event.touches.length) {
694				return;
695			}
696
697			isTouchDragging = false;
698			benefitsMomentum.cancel();
699			touchStartX = event.touches[0].clientX;
700			touchStartY = event.touches[0].clientY;
701			touchStartTranslate = currentTranslate;
702			benefitsMomentum.begin(currentTranslate);
703			suppressBenefitsClick = false;
704		}, { passive: true });
705
706		benefitsViewport.addEventListener('touchmove', function (event) {
707			if (benefitsAnimating || !event.touches.length) {
708				return;
709			}
710
711			const deltaX = event.touches[0].clientX - touchStartX;
712			const deltaY = event.touches[0].clientY - touchStartY;
713
714			if (!isTouchDragging && Math.abs(deltaX) <= Math.abs(deltaY)) {
715				return;
716			}
717
718			if (Math.abs(deltaX) > carouselDragClickThreshold) {
719				isTouchDragging = true;
720				suppressBenefitsClick = true;
721			}
722
723			if (!isTouchDragging) {
724				return;
725			}
726
727			setTrackTranslate(touchStartTranslate - deltaX);
728			benefitsMomentum.record(currentTranslate);
729			event.preventDefault();
730		}, { passive: false });
731
732		benefitsViewport.addEventListener('touchend', function () {
733			if (!isTouchDragging || benefitsAnimating) {
734				benefitsMomentum.cancel();
735				return;
736			}
737
738			isTouchDragging = false;
739			benefitsMomentum.release(currentTranslate);
740		});
741
742		benefitsViewport.addEventListener('touchcancel', function () {
743			isTouchDragging = false;
744			benefitsMomentum.cancel();
745			syncBenefitsState();
746		});
747
748		benefitsCards.forEach(function (card, index) {
749			card.addEventListener('click', function () {
750				if (suppressBenefitsClick) {
751					suppressBenefitsClick = false;
752					return;
753				}
754
755				animateToSnapIndex(index);
756			});
757		});
758
759		let benefitsResizeTimer = null;
760		let benefitsLastViewportWidth = window.innerWidth;
761
762		window.addEventListener('resize', function () {
763			const nextViewportWidth = window.innerWidth;
764			const widthChanged = Math.abs(nextViewportWidth - benefitsLastViewportWidth) > 1;
765
766			benefitsLastViewportWidth = nextViewportWidth;
767			window.clearTimeout(benefitsResizeTimer);
768
769			if (!widthChanged) {
770				return;
771			}
772
773			benefitsResizeTimer = window.setTimeout(function () {
774				const snapPoints = getSnapPoints();
775
776				benefitsMomentum.cancel();
777				currentBenefitsIndex = clampBenefitsIndex(currentBenefitsIndex);
778				setTrackTranslate(snapPoints[currentBenefitsIndex] || 0);
779				updateBenefitsButtons();
780			}, 150);
781		});
782
783		setTrackTranslate(0);
784		updateBenefitsButtons();
785	}
786
787	if (useCasesViewport && useCasesTrack && useCasesPrevButton && useCasesNextButton) {
788		const useCasesCards = Array.from(
789			useCasesTrack.querySelectorAll('.tt-home-use-cases-card')
790		);
791
792		let currentUseCasesIndex = 0;
793		let currentUseCasesTranslate = 0;
794		let useCasesAnimating = false;
795		let useCasesDragStartX = 0;
796		let useCasesDragStartTranslate = 0;
797		let useCasesTouchStartX = 0;
798		let useCasesTouchStartY = 0;
799		let useCasesTouchStartTranslate = 0;
800		let useCasesIsTouchDragging = false;
801		let useCasesIsDragging = false;
802		let suppressUseCasesClick = false;
803
804		function clampUseCasesIndex(index) {
805			return Math.max(0, Math.min(index, useCasesCards.length - 1));
806		}
807
808		function getUseCasesSnapPoints() {
809			return useCasesCards.map(function (card) {
810				return Math.round(card.offsetLeft);
811			});
812		}
813
814		function getUseCasesMaxTranslate() {
815			return Math.max(0, useCasesTrack.scrollWidth - useCasesViewport.clientWidth);
816		}
817
818		function setUseCasesTrackTranslate(value) {
819			const maxTranslate = getUseCasesMaxTranslate();
820			currentUseCasesTranslate = Math.max(0, Math.min(value, maxTranslate));
821			useCasesTrack.style.transform = 'translate3d(' + (-currentUseCasesTranslate) + 'px, 0, 0)';
822		}
823
824		function updateUseCasesButtons() {
825			useCasesPrevButton.disabled = currentUseCasesIndex <= 0;
826			useCasesNextButton.disabled = currentUseCasesIndex >= useCasesCards.length - 1;
827		}
828
829		function animateToUseCasesSnapIndex(index) {
830			const snapPoints = getUseCasesSnapPoints();
831			const safeIndex = clampUseCasesIndex(index);
832			const targetTranslate = snapPoints[safeIndex];
833			const startTranslate = currentUseCasesTranslate;
834			const distance = targetTranslate - startTranslate;
835			const duration = carouselReducedMotion.matches ? 0 : 520;
836
837			useCasesMomentum.cancel();
838
839			if (useCasesAnimating) {
840				return;
841			}
842
843			currentUseCasesIndex = safeIndex;
844			updateUseCasesButtons();
845
846			if (Math.abs(distance) < 1 || duration === 0) {
847				setUseCasesTrackTranslate(targetTranslate);
848				return;
849			}
850
851			useCasesAnimating = true;
852			const startTime = performance.now();
853
854			function step(now) {
855				const elapsed = now - startTime;
856				const progress = Math.min(elapsed / duration, 1);
857				const eased = easeInOutCubic(progress);
858
859				setUseCasesTrackTranslate(startTranslate + (distance * eased));
860
861				if (progress < 1) {
862					requestAnimationFrame(step);
863				} else {
864					setUseCasesTrackTranslate(targetTranslate);
865					useCasesAnimating = false;
866				}
867			}
868
869			requestAnimationFrame(step);
870		}
871
872		function getNearestUseCasesSnapIndex() {
873			const snapPoints = getUseCasesSnapPoints();
874			const maxTranslate = getUseCasesMaxTranslate();
875
876			if (currentUseCasesTranslate <= 1) {
877				return 0;
878			}
879
880			if (currentUseCasesTranslate >= maxTranslate - 1) {
881				return useCasesCards.length - 1;
882			}
883
884			let nearestIndex = 0;
885			let nearestDistance = Infinity;
886
887			snapPoints.forEach(function (point, index) {
888				const distance = Math.abs(point - currentUseCasesTranslate);
889
890				if (distance < nearestDistance) {
891					nearestDistance = distance;
892					nearestIndex = index;
893				}
894			});
895
896			return nearestIndex;
897		}
898
899		function syncUseCasesState() {
900			currentUseCasesIndex = clampUseCasesIndex(getNearestUseCasesSnapIndex());
901			updateUseCasesButtons();
902		}
903
904		const useCasesMomentum = window.TTCarouselMomentum.create({
905			getMin: function () {
906				return 0;
907			},
908			getMax: getUseCasesMaxTranslate,
909			setPosition: setUseCasesTrackTranslate,
910			onSettle: syncUseCasesState
911		});
912
913		useCasesPrevButton.addEventListener('click', function () {
914			if (useCasesAnimating) {
915				return;
916			}
917			useCasesMomentum.cancel();
918			syncUseCasesState();
919			animateToUseCasesSnapIndex(currentUseCasesIndex - 1);
920		});
921
922		useCasesNextButton.addEventListener('click', function () {
923			if (useCasesAnimating) {
924				return;
925			}
926			useCasesMomentum.cancel();
927			syncUseCasesState();
928			animateToUseCasesSnapIndex(currentUseCasesIndex + 1);
929		});
930
931		useCasesViewport.setAttribute('aria-label', 'Use cases carousel');
932		useCasesViewport.addEventListener('keydown', function (event) {
933			if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
934				return;
935			}
936
937			event.preventDefault();
938			useCasesMomentum.cancel();
939			syncUseCasesState();
940			animateToUseCasesSnapIndex(currentUseCasesIndex + (event.key === 'ArrowRight' ? 1 : -1));
941		});
942
943		useCasesViewport.addEventListener('pointerdown', function (event) {
944			if (useCasesAnimating || event.pointerType === 'touch') {
945				return;
946			}
947
948			useCasesIsDragging = true;
949			useCasesMomentum.cancel();
950			useCasesDragStartX = event.clientX;
951			useCasesDragStartTranslate = currentUseCasesTranslate;
952			useCasesMomentum.begin(currentUseCasesTranslate);
953			suppressUseCasesClick = false;
954			useCasesViewport.style.cursor = 'grabbing';
955		});
956
957		window.addEventListener('pointermove', function (event) {
958			if (!useCasesIsDragging || useCasesAnimating) {
959				return;
960			}
961
962			const deltaX = event.clientX - useCasesDragStartX;
963
964			if (Math.abs(deltaX) > carouselDragClickThreshold) {
965				suppressUseCasesClick = true;
966			}
967
968			setUseCasesTrackTranslate(useCasesDragStartTranslate - deltaX);
969			useCasesMomentum.record(currentUseCasesTranslate);
970		});
971
972		window.addEventListener('pointerup', function () {
973			if (!useCasesIsDragging) {
974				return;
975			}
976
977			useCasesIsDragging = false;
978			useCasesViewport.style.cursor = '';
979
980			if (suppressUseCasesClick) {
981				useCasesMomentum.release(currentUseCasesTranslate);
982			} else {
983				useCasesMomentum.cancel();
984				syncUseCasesState();
985			}
986		});
987
988		useCasesViewport.addEventListener('touchstart', function (event) {
989			if (useCasesAnimating || !event.touches.length) {
990				return;
991			}
992
993			useCasesIsTouchDragging = false;
994			useCasesMomentum.cancel();
995			useCasesTouchStartX = event.touches[0].clientX;
996			useCasesTouchStartY = event.touches[0].clientY;
997			useCasesTouchStartTranslate = currentUseCasesTranslate;
998			useCasesMomentum.begin(currentUseCasesTranslate);
999			suppressUseCasesClick = false;
1000		}, { passive: true });
1001
1002		useCasesViewport.addEventListener('touchmove', function (event) {
1003			if (useCasesAnimating || !event.touches.length) {
1004				return;
1005			}
1006
1007			const deltaX = event.touches[0].clientX - useCasesTouchStartX;
1008			const deltaY = event.touches[0].clientY - useCasesTouchStartY;
1009
1010			if (!useCasesIsTouchDragging && Math.abs(deltaX) <= Math.abs(deltaY)) {
1011				return;
1012			}
1013
1014			if (Math.abs(deltaX) > carouselDragClickThreshold) {
1015				useCasesIsTouchDragging = true;
1016				suppressUseCasesClick = true;
1017			}
1018
1019			if (!useCasesIsTouchDragging) {
1020				return;
1021			}
1022
1023			setUseCasesTrackTranslate(useCasesTouchStartTranslate - deltaX);
1024			useCasesMomentum.record(currentUseCasesTranslate);
1025			event.preventDefault();
1026		}, { passive: false });
1027
1028		useCasesViewport.addEventListener('touchend', function () {
1029			if (!useCasesIsTouchDragging || useCasesAnimating) {
1030				useCasesMomentum.cancel();
1031				return;
1032			}
1033
1034			useCasesIsTouchDragging = false;
1035			useCasesMomentum.release(currentUseCasesTranslate);
1036		});
1037
1038		useCasesViewport.addEventListener('touchcancel', function () {
1039			useCasesIsTouchDragging = false;
1040			useCasesMomentum.cancel();
1041			syncUseCasesState();
1042		});
1043
1044		useCasesCards.forEach(function (card, index) {
1045			card.addEventListener('click', function () {
1046				if (suppressUseCasesClick) {
1047					suppressUseCasesClick = false;
1048					return;
1049				}
1050
1051				animateToUseCasesSnapIndex(index);
1052			});
1053		});
1054
1055		let useCasesResizeTimer = null;
1056		let useCasesLastViewportWidth = window.innerWidth;
1057
1058		window.addEventListener('resize', function () {
1059			const nextViewportWidth = window.innerWidth;
1060			const widthChanged = Math.abs(nextViewportWidth - useCasesLastViewportWidth) > 1;
1061
1062			useCasesLastViewportWidth = nextViewportWidth;
1063			window.clearTimeout(useCasesResizeTimer);
1064
1065			if (!widthChanged) {
1066				return;
1067			}
1068
1069			useCasesResizeTimer = window.setTimeout(function () {
1070				const snapPoints = getUseCasesSnapPoints();
1071
1072				useCasesMomentum.cancel();
1073				currentUseCasesIndex = clampUseCasesIndex(currentUseCasesIndex);
1074				setUseCasesTrackTranslate(snapPoints[currentUseCasesIndex] || 0);
1075				updateUseCasesButtons();
1076			}, 150);
1077		});
1078
1079		setUseCasesTrackTranslate(0);
1080		updateUseCasesButtons();
1081	}
1082
1083	if (userStoriesCarousel && userStoriesTrack) {
1084		userStoriesCarousel.addEventListener('dragstart', function (event) {
1085			if (event.target.closest('.tt-home-user-stories-card')) {
1086				event.preventDefault();
1087			}
1088		});
1089
1090		const originalCards = Array.from(userStoriesTrack.children);
1091		const duplicateCards = originalCards.map(function (card) {
1092			return card.cloneNode(true);
1093		});
1094
1095		duplicateCards.forEach(function (card) {
1096			userStoriesTrack.appendChild(card);
1097		});
1098
1099		let storyPosition = 0;
1100		let isHovered = false;
1101		let isDragging = false;
1102		let isTouchDragging = false;
1103		let dragStartX = 0;
1104		let dragStartTranslate = 0;
1105		let suppressStoryClick = false;
1106		let touchStartX = 0;
1107		let touchStartY = 0;
1108		let touchStartTranslate = 0;
1109		let animationFrameId = null;
1110		const speed = 0.45;
1111
1112		function getLoopWidth() {
1113			const cards = Array.from(userStoriesTrack.children);
1114			let total = 0;
1115
1116			for (let i = 0; i < cards.length / 2; i += 1) {
1117				total += cards[i].offsetWidth;
1118				if (i < (cards.length / 2) - 1) {
1119					total += 24;
1120				}
1121			}
1122
1123			return total;
1124		}
1125
1126		function setUserStoriesTranslate(value, shouldWrap) {
1127			const loopWidth = getLoopWidth();
1128			let displayPosition = value;
1129
1130			storyPosition = value;
1131
1132			if (loopWidth > 0 && shouldWrap !== false) {
1133				displayPosition %= loopWidth;
1134
1135				if (displayPosition > 0) {
1136					displayPosition -= loopWidth;
1137				}
1138			}
1139
1140			userStoriesTrack.style.transform = 'translate3d(' + displayPosition + 'px, 0, 0)';
1141		}
1142
1143		const storyMomentum = window.TTCarouselMomentum.create({
1144			setPosition: setUserStoriesTranslate
1145		});
1146
1147		function tickUserStories() {
1148			if (!isHovered && !isDragging && !isTouchDragging && !storyMomentum.isRunning()) {
1149				setUserStoriesTranslate(storyPosition - speed);
1150			}
1151
1152			animationFrameId = window.requestAnimationFrame(tickUserStories);
1153		}
1154
1155		userStoriesCarousel.addEventListener('mouseenter', function () {
1156			isHovered = true;
1157		});
1158
1159		userStoriesCarousel.addEventListener('mouseleave', function () {
1160			isHovered = false;
1161		});
1162
1163		userStoriesCarousel.addEventListener('pointerdown', function (event) {
1164			if (event.pointerType === 'touch') {
1165				return;
1166			}
1167
1168			storyMomentum.cancel();
1169			isDragging = true;
1170			suppressStoryClick = false;
1171			dragStartX = event.clientX;
1172			dragStartTranslate = storyPosition;
1173			storyMomentum.begin(storyPosition);
1174			userStoriesCarousel.style.cursor = 'grabbing';
1175		});
1176
1177		window.addEventListener('pointermove', function (event) {
1178			if (!isDragging) {
1179				return;
1180			}
1181
1182			const deltaX = event.clientX - dragStartX;
1183
1184			if (Math.abs(deltaX) > 6) {
1185				suppressStoryClick = true;
1186			}
1187
1188			setUserStoriesTranslate(dragStartTranslate + deltaX, false);
1189			storyMomentum.record(storyPosition);
1190		});
1191
1192		window.addEventListener('pointerup', function () {
1193			if (!isDragging) {
1194				return;
1195			}
1196
1197			isDragging = false;
1198			userStoriesCarousel.style.cursor = '';
1199
1200			if (suppressStoryClick) {
1201				storyMomentum.release(storyPosition);
1202			} else {
1203				storyMomentum.cancel();
1204			}
1205		});
1206
1207		userStoriesCarousel.addEventListener('touchstart', function (event) {
1208			if (!event.touches.length) {
1209				return;
1210			}
1211
1212			storyMomentum.cancel();
1213			isTouchDragging = false;
1214			suppressStoryClick = false;
1215			touchStartX = event.touches[0].clientX;
1216			touchStartY = event.touches[0].clientY;
1217			touchStartTranslate = storyPosition;
1218			storyMomentum.begin(storyPosition);
1219		}, { passive: true });
1220
1221		userStoriesCarousel.addEventListener('touchmove', function (event) {
1222			if (!event.touches.length) {
1223				return;
1224			}
1225
1226			const deltaX = event.touches[0].clientX - touchStartX;
1227			const deltaY = event.touches[0].clientY - touchStartY;
1228
1229			if (!isTouchDragging && Math.abs(deltaX) <= Math.abs(deltaY)) {
1230				return;
1231			}
1232
1233			if (Math.abs(deltaX) <= carouselDragClickThreshold) {
1234				return;
1235			}
1236
1237			isTouchDragging = true;
1238			suppressStoryClick = true;
1239			setUserStoriesTranslate(touchStartTranslate + deltaX, false);
1240			storyMomentum.record(storyPosition);
1241			event.preventDefault();
1242		}, { passive: false });
1243
1244		userStoriesCarousel.addEventListener('touchend', function () {
1245			if (isTouchDragging) {
1246				storyMomentum.release(storyPosition);
1247			} else {
1248				storyMomentum.cancel();
1249			}
1250
1251			isTouchDragging = false;
1252		});
1253
1254		userStoriesCarousel.addEventListener('touchcancel', function () {
1255			isTouchDragging = false;
1256			storyMomentum.cancel();
1257		});
1258
1259		userStoriesCarousel.addEventListener('click', function (event) {
1260			if (!suppressStoryClick) {
1261				return;
1262			}
1263
1264			event.preventDefault();
1265			suppressStoryClick = false;
1266		}, true);
1267
1268		window.addEventListener('blur', function () {
1269			isDragging = false;
1270			isTouchDragging = false;
1271			storyMomentum.cancel();
1272			userStoriesCarousel.style.cursor = '';
1273		});
1274
1275		animationFrameId = window.requestAnimationFrame(tickUserStories);
1276	}
1277
1278const audienceCards = document.querySelector('.tt-home-audience-cards');
1279const audiencePrev = document.querySelector('.js-audience-prev');
1280const audienceNext = document.querySelector('.js-audience-next');
1281
1282if (audienceCards && audiencePrev && audienceNext) {
1283	let currentAudienceIndex = 0;
1284	let audienceScrollTimer;
1285	let audienceDragStartX = NaN;
1286	let audienceDragStartScrollLeft = 0;
1287	let audiencePointerId = null;
1288	let isAudienceDragging = false;
1289	let suppressAudienceClick = false;
1290	const audienceItems = Array.from(audienceCards.querySelectorAll('a'));
1291
1292	function shouldUseAudienceCarousel() {
1293		return window.matchMedia('(max-width: 599px)').matches;
1294	}
1295
1296	function getAudienceMaxScrollLeft() {
1297		return Math.max(0, audienceCards.scrollWidth - audienceCards.clientWidth);
1298	}
1299
1300	function getAudienceItemLeft(index) {
1301		const item = audienceItems[index];
1302
1303		if (!item) {
1304			return 0;
1305		}
1306
1307		return Math.max(0, Math.min(item.offsetLeft, getAudienceMaxScrollLeft()));
1308	}
1309
1310	function getClosestAudienceIndex() {
1311		const maxScrollLeft = getAudienceMaxScrollLeft();
1312
1313		if (audienceCards.scrollLeft <= 1) {
1314			return 0;
1315		}
1316
1317		if (audienceCards.scrollLeft >= maxScrollLeft - 1) {
1318			return audienceItems.length - 1;
1319		}
1320
1321		let closestIndex = 0;
1322		let closestDistance = Infinity;
1323
1324		audienceItems.forEach(function (item, index) {
1325			const distance = Math.abs(audienceCards.scrollLeft - getAudienceItemLeft(index));
1326
1327			if (distance < closestDistance) {
1328				closestDistance = distance;
1329				closestIndex = index;
1330			}
1331		});
1332
1333		return closestIndex;
1334	}
1335
1336	function syncAudienceState() {
1337		currentAudienceIndex = getClosestAudienceIndex();
1338		updateAudienceButtons();
1339	}
1340
1341	const audienceMomentum = window.TTCarouselMomentum.create({
1342		getMin: function () {
1343			return 0;
1344		},
1345		getMax: getAudienceMaxScrollLeft,
1346		setPosition: function (value) {
1347			audienceCards.scrollLeft = value;
1348		},
1349		onSettle: syncAudienceState
1350	});
1351
1352	function goToAudienceCard(index, behavior) {
1353		behavior = behavior || (carouselReducedMotion.matches ? 'auto' : 'smooth');
1354		audienceMomentum.cancel();
1355
1356		currentAudienceIndex = Math.max(
1357			0,
1358			Math.min(index, audienceItems.length - 1)
1359		);
1360
1361		audienceCards.scrollTo({
1362			left: getAudienceItemLeft(currentAudienceIndex),
1363			behavior: behavior
1364		});
1365
1366		updateAudienceButtons();
1367	}
1368
1369	audiencePrev.addEventListener('click', function () {
1370		syncAudienceState();
1371		goToAudienceCard(currentAudienceIndex - 1);
1372	});
1373
1374	audienceNext.addEventListener('click', function () {
1375		syncAudienceState();
1376		goToAudienceCard(currentAudienceIndex + 1);
1377	});
1378
1379	audienceCards.addEventListener('scroll', function () {
1380		if (audienceCards.scrollLeft > 1) {
1381			audienceCards.classList.add('audience-scrolled-right');
1382		} else {
1383			audienceCards.classList.remove('audience-scrolled-right');
1384		}
1385
1386		window.clearTimeout(audienceScrollTimer);
1387		audienceScrollTimer = window.setTimeout(function () {
1388			currentAudienceIndex = getClosestAudienceIndex();
1389			updateAudienceButtons();
1390		}, 80);
1391	});
1392
1393	function updateAudienceButtons() {
1394		const maxScrollLeft = getAudienceMaxScrollLeft();
1395
1396		currentAudienceIndex = getClosestAudienceIndex();
1397
1398		audiencePrev.disabled = maxScrollLeft <= 1 || audienceCards.scrollLeft <= 1;
1399		audienceNext.disabled = maxScrollLeft <= 1 || audienceCards.scrollLeft >= maxScrollLeft - 1;
1400	}
1401
1402	audienceItems.forEach(function (item, index) {
1403		item.addEventListener('dragstart', function (event) {
1404			if (shouldUseAudienceCarousel()) {
1405				event.preventDefault();
1406			}
1407		});
1408
1409		item.addEventListener('click', function (event) {
1410			if (suppressAudienceClick) {
1411				event.preventDefault();
1412				suppressAudienceClick = false;
1413				return;
1414			}
1415
1416			if (!shouldUseAudienceCarousel()) {
1417				return;
1418			}
1419
1420			const activeIndex = getClosestAudienceIndex();
1421
1422			// If card isn't active, scroll to it
1423			if (index !== activeIndex) {
1424				event.preventDefault();
1425				goToAudienceCard(index);
1426			}
1427		});
1428	});
1429
1430	audienceCards.addEventListener('pointerdown', function (event) {
1431		if (
1432			!shouldUseAudienceCarousel() ||
1433			event.pointerType !== 'mouse' ||
1434			event.button !== 0
1435		) {
1436			return;
1437		}
1438
1439		audiencePointerId = event.pointerId;
1440		audienceDragStartX = event.clientX;
1441		audienceDragStartScrollLeft = audienceCards.scrollLeft;
1442		isAudienceDragging = false;
1443		suppressAudienceClick = false;
1444		audienceMomentum.cancel();
1445		audienceMomentum.begin(audienceCards.scrollLeft);
1446
1447		if (audienceCards.setPointerCapture) {
1448			audienceCards.setPointerCapture(audiencePointerId);
1449		}
1450	});
1451
1452	audienceCards.addEventListener('pointermove', function (event) {
1453		if (
1454			!shouldUseAudienceCarousel() ||
1455			!Number.isFinite(audienceDragStartX) ||
1456			event.pointerId !== audiencePointerId
1457		) {
1458			return;
1459		}
1460
1461		const deltaX = event.clientX - audienceDragStartX;
1462
1463		if (Math.abs(deltaX) > carouselDragClickThreshold) {
1464			isAudienceDragging = true;
1465			suppressAudienceClick = true;
1466			audienceCards.classList.add('is-dragging');
1467			event.preventDefault();
1468		}
1469
1470		if (isAudienceDragging) {
1471			audienceCards.scrollLeft = audienceDragStartScrollLeft - deltaX;
1472			audienceMomentum.record(audienceCards.scrollLeft);
1473		}
1474	});
1475
1476	function stopAudienceDrag(event) {
1477		if (event.pointerId !== audiencePointerId) {
1478			return;
1479		}
1480
1481		const shouldGlide = isAudienceDragging;
1482
1483		audienceDragStartX = NaN;
1484		audiencePointerId = null;
1485		isAudienceDragging = false;
1486		audienceCards.classList.remove('is-dragging');
1487
1488		if (audienceCards.hasPointerCapture && audienceCards.hasPointerCapture(event.pointerId)) {
1489			audienceCards.releasePointerCapture(event.pointerId);
1490		}
1491
1492		if (shouldGlide && event.type !== 'pointercancel') {
1493			audienceMomentum.release(audienceCards.scrollLeft);
1494		} else {
1495			audienceMomentum.cancel();
1496			syncAudienceState();
1497		}
1498	}
1499
1500	audienceCards.addEventListener('pointerup', stopAudienceDrag);
1501	audienceCards.addEventListener('pointercancel', stopAudienceDrag);
1502
1503	let audienceResizeTimer = null;
1504	let audienceLastViewportWidth = window.innerWidth;
1505
1506	window.addEventListener('resize', function () {
1507		const nextViewportWidth = window.innerWidth;
1508		const widthChanged = Math.abs(nextViewportWidth - audienceLastViewportWidth) > 1;
1509
1510		audienceLastViewportWidth = nextViewportWidth;
1511		window.clearTimeout(audienceResizeTimer);
1512
1513		if (!widthChanged) {
1514			return;
1515		}
1516
1517		audienceResizeTimer = window.setTimeout(function () {
1518			audienceMomentum.cancel();
1519			currentAudienceIndex = getClosestAudienceIndex();
1520
1521			audienceCards.scrollTo({
1522				left: getAudienceItemLeft(currentAudienceIndex),
1523				behavior: 'auto'
1524			});
1525
1526			updateAudienceButtons();
1527		}, 150);
1528	});
1529
1530	updateAudienceButtons();
1531}
1532
1533	function shouldUseFeatureStack() {
1534		return true;
1535	}
1536
1537	function getFeatureActiveCardIndex(currentCompression, compressionStops) {
1538		let activeIndex = 0;
1539
1540		for (let index = 1; index < compressionStops.length; index++) {
1541			const previousStop = compressionStops[index - 1];
1542			const currentStop = compressionStops[index];
1543			const activationPoint = previousStop + ((currentStop - previousStop) / 2);
1544
1545			if (currentCompression >= activationPoint) {
1546				activeIndex = index;
1547			}
1548		}
1549
1550		return activeIndex;
1551	}
1552
1553	function getCurrentFeatureCardIndex() {
1554		if (!featureScrollArea || !featureCards.length) {
1555			return 0;
1556		}
1557
1558		const settings = getFeatureStackSettings();
1559		const rect = featureScrollArea.getBoundingClientRect();
1560		const overallProgress = clamp((-rect.top) / cachedFeatureAvailableScroll, 0, 1);
1561		const currentCompression = settings.totalCompression * overallProgress;
1562
1563		return getFeatureActiveCardIndex(currentCompression, settings.compressionStops);
1564	}
1565
1566	function updateFeatureStackAccessibility(index, announce) {
1567		if (!featureCards.length) {
1568			return;
1569		}
1570
1571		const safeIndex = clamp(index, 0, featureCards.length - 1);
1572		const activeCard = featureCards[safeIndex];
1573		const heading = activeCard.querySelector('.tt-home-feature-card__heading, h3');
1574		const headingText = heading ? heading.textContent.trim() : 'Card ' + (safeIndex + 1);
1575
1576		activeFeatureCardIndex = safeIndex;
1577
1578		featureCards.forEach(function (card, cardIndex) {
1579			if (cardIndex === safeIndex) {
1580				card.setAttribute('aria-current', 'step');
1581			} else {
1582				card.removeAttribute('aria-current');
1583			}
1584		});
1585
1586		if (featureStackStatus) {
1587			const statusText = 'Current card: ' + (safeIndex + 1) + ' of '
1588				+ featureCards.length + ', ' + headingText + '.';
1589
1590			featureStackStatus.setAttribute('aria-live', announce ? 'polite' : 'off');
1591
1592			if (featureStackStatus.textContent !== statusText) {
1593				featureStackStatus.textContent = statusText;
1594			}
1595		}
1596	}
1597
1598	function scrollToFeatureCard(index) {
1599		if (!featureScrollArea || !featureCards.length) {
1600			return;
1601		}
1602
1603		const settings = getFeatureStackSettings();
1604		const safeIndex = clamp(index, 0, featureCards.length - 1);
1605		const targetTop = featureScrollArea.getBoundingClientRect().top
1606			+ window.scrollY
1607			+ settings.compressionStops[safeIndex];
1608		const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
1609
1610		updateFeatureStackAccessibility(safeIndex, true);
1611
1612		window.scrollTo({
1613			top: Math.max(0, targetTop),
1614			behavior: prefersReducedMotion ? 'auto' : 'smooth'
1615		});
1616	}
1617
1618	function setupFeatureStackAccessibility() {
1619		if (!featureStackSection || !featureStack || !featureCards.length) {
1620			return;
1621		}
1622
1623		const sectionHeading = featureStackSection.querySelector('.tt-home-features__title, h2');
1624		const instructions = document.createElement('span');
1625		featureStackStatus = document.createElement('span');
1626
1627		if (sectionHeading && !sectionHeading.id) {
1628			sectionHeading.id = 'tt-feature-stack-heading';
1629		}
1630
1631		instructions.id = 'tt-feature-stack-instructions';
1632		instructions.className = 'tt-home-features__keyboard-instructions';
1633		instructions.textContent = 'Use the arrow keys to move through the cards. Press Home for the first card or End for the last card.';
1634
1635		featureStackStatus.id = 'tt-feature-stack-status';
1636		featureStackStatus.className = 'tt-home-features__keyboard-instructions';
1637		featureStackStatus.setAttribute('aria-atomic', 'true');
1638
1639		featureStack.appendChild(instructions);
1640		featureStack.appendChild(featureStackStatus);
1641		featureStack.setAttribute('tabindex', '0');
1642		featureStack.setAttribute('role', 'group');
1643		featureStack.setAttribute('aria-describedby', instructions.id + ' ' + featureStackStatus.id);
1644
1645		if (sectionHeading) {
1646			featureStack.setAttribute('aria-labelledby', sectionHeading.id);
1647		} else {
1648			featureStack.setAttribute('aria-label', 'Stacked cards');
1649		}
1650
1651		featureStack.addEventListener('focus', function () {
1652			updateFeatureStackAccessibility(getCurrentFeatureCardIndex(), false);
1653		});
1654
1655		featureStack.addEventListener('keydown', function (event) {
1656			if (event.target !== featureStack) {
1657				return;
1658			}
1659
1660			let nextIndex = activeFeatureCardIndex;
1661
1662			switch (event.key) {
1663				case 'ArrowDown':
1664				case 'ArrowRight':
1665					nextIndex += 1;
1666					break;
1667				case 'ArrowUp':
1668				case 'ArrowLeft':
1669					nextIndex -= 1;
1670					break;
1671				case 'Home':
1672					nextIndex = 0;
1673					break;
1674				case 'End':
1675					nextIndex = featureCards.length - 1;
1676					break;
1677				default:
1678					return;
1679			}
1680
1681			event.preventDefault();
1682			scrollToFeatureCard(nextIndex);
1683		});
1684
1685		updateFeatureStackAccessibility(0, false);
1686	}
1687
1688	function getFeatureCardDepth(cardIndex, currentCompression, compressionStops) {
1689		let depth = 0;
1690
1691		for (let nextCardIndex = cardIndex + 1; nextCardIndex < compressionStops.length; nextCardIndex++) {
1692			const stageStart = compressionStops[nextCardIndex - 1];
1693			const stageEnd = compressionStops[nextCardIndex];
1694			const stageProgress = clamp(
1695				(currentCompression - stageStart) / Math.max(1, stageEnd - stageStart),
1696				0,
1697				1
1698			);
1699
1700			depth += stageProgress;
1701		}
1702
1703		return depth;
1704	}
1705
1706	function getFeatureStackSettings() {
1707		if (cachedFeatureStackSettings) {
1708			return cachedFeatureStackSettings;
1709		}
1710
1711		const styles = featureStackSection
1712			? window.getComputedStyle(featureStackSection)
1713			: null;
1714
1715		const fallbackCardHeight = styles
1716			? parseFloat(styles.getPropertyValue('--feature-card-height')) || 532
1717			: 532;
1718		const cardGap = styles
1719			? parseFloat(styles.getPropertyValue('--feature-card-gap')) || 24
1720			: 24;
1721		const lipSize = styles
1722			? parseFloat(styles.getPropertyValue('--feature-lip-size')) || 20
1723			: 20;
1724		const cardHeights = featureCards.map(function (card) {
1725			return card.offsetHeight || fallbackCardHeight;
1726		});
1727		const naturalPositions = [];
1728		const compressedPositions = [];
1729		const compressionStops = [];
1730		let nextNaturalPosition = 0;
1731
1732		cardHeights.forEach(function (cardHeight, index) {
1733			naturalPositions.push(nextNaturalPosition);
1734			compressedPositions.push(index * lipSize);
1735			compressionStops.push(nextNaturalPosition - (index * lipSize));
1736			nextNaturalPosition += cardHeight + cardGap;
1737		});
1738
1739		const totalCompression = compressionStops.length
1740			? compressionStops[compressionStops.length - 1]
1741			: 0;
1742		const packedStackHeight = cardHeights.reduce(function (height, cardHeight, index) {
1743			return Math.max(height, compressedPositions[index] + cardHeight);
1744		}, 0);
1745
1746		cachedFeatureStackSettings = {
1747			cardHeights: cardHeights,
1748			naturalPositions: naturalPositions,
1749			compressedPositions: compressedPositions,
1750			compressionStops: compressionStops,
1751			totalCompression: totalCompression,
1752			packedStackHeight: packedStackHeight,
1753			stickyTop: styles
1754				? parseFloat(styles.getPropertyValue('--feature-sticky-top')) || 64
1755				: 64
1756		};
1757
1758		return cachedFeatureStackSettings;
1759	}
1760
1761	function resetFeatureStackInlineStyles() {
1762		if (!featureScrollArea || !featureStack || !featureSticky || !featureCards.length) {
1763			return;
1764		}
1765
1766		featureScrollArea.style.height = '';
1767		featureStack.style.height = '';
1768		featureSticky.style.top = '';
1769		featureSticky.style.height = '';
1770
1771		featureCards.forEach(function (card) {
1772			card.style.transform = '';
1773			card.style.zIndex = '';
1774			card.style.boxShadow = '';
1775		});
1776	}
1777
1778	function setupFeatureStackLayout() {
1779		if (!featureScrollArea || !featureStack || !featureSticky || !featureCards.length) {
1780			return;
1781		}
1782
1783		if (!shouldUseFeatureStack()) {
1784			resetFeatureStackInlineStyles();
1785			return;
1786		}
1787
1788		const settings = getFeatureStackSettings();
1789
1790		const stickyTop = settings.stickyTop;
1791		const compressionDistance = settings.totalCompression;
1792		const packedStackHeight = settings.packedStackHeight;
1793
1794		featureStack.style.height = packedStackHeight + 'px';
1795		featureSticky.style.top = stickyTop + 'px';
1796		featureSticky.style.height = packedStackHeight + 'px';
1797		featureScrollArea.style.height = (packedStackHeight + compressionDistance) + 'px';
1798
1799		cachedFeatureAvailableScroll = Math.max(1, compressionDistance);
1800
1801		featureCards.forEach(function (card, index) {
1802			card.style.zIndex = String(100 + index);
1803		});
1804	}
1805
1806	function getStableViewportHeight() {
1807		return document.documentElement.clientHeight || window.innerHeight;
1808	}
1809
1810	function refreshHeroMeasurements() {
1811		if (!hero) {
1812			return;
1813		}
1814
1815		const heroStyles = window.getComputedStyle(hero);
1816		const animationDistanceRatio = parseFloat(heroStyles.getPropertyValue('--hero-animation-distance'));
1817		const settledWidth = parseFloat(heroStyles.getPropertyValue('--hero-settled-width'));
1818		const settledHeight = parseFloat(heroStyles.getPropertyValue('--hero-settled-height'));
1819		const visualTop = heroVisual ? heroVisual.offsetTop : 0;
1820		const preScrollHeight = Math.max(680, cachedViewportHeight - visualTop);
1821
1822		cachedHeroSettledWidth = Number.isFinite(settledWidth) ? settledWidth : 1260;
1823		cachedHeroSettledHeight = Number.isFinite(settledHeight) ? settledHeight : 680;
1824		cachedHeroPreScrollWidth = heroVisual
1825			? heroVisual.getBoundingClientRect().width
1826			: window.innerWidth;
1827		cachedHeroPreScrollHeight = preScrollHeight;
1828		cachedHeroAnimationDistance = cachedViewportHeight
1829			* (Number.isFinite(animationDistanceRatio) ? animationDistanceRatio : 0.54);
1830	}
1831
1832	function refreshScrollAnimationLayout() {
1833		cachedFeatureStackSettings = null;
1834		cachedFeatureAvailableScroll = 1;
1835		cachedViewportHeight = getStableViewportHeight();
1836
1837		refreshHeroMeasurements();
1838
1839		setupFeatureStackLayout();
1840		updateScrollAnimations();
1841	}
1842
1843	let scrollAnimationResizeTimer = null;
1844	let scrollAnimationLastViewportWidth = window.innerWidth;
1845
1846	setupFeatureStackAccessibility();
1847	refreshScrollAnimationLayout();
1848
1849	window.addEventListener('scroll', requestAnimTick, { passive: true });
1850
1851	window.addEventListener('resize', function () {
1852		const nextViewportWidth = window.innerWidth;
1853		const widthChanged = Math.abs(nextViewportWidth - scrollAnimationLastViewportWidth) > 1;
1854
1855		scrollAnimationLastViewportWidth = nextViewportWidth;
1856		window.clearTimeout(scrollAnimationResizeTimer);
1857
1858		if (!widthChanged) {
1859			scrollAnimationResizeTimer = window.setTimeout(function () {
1860				cachedViewportHeight = getStableViewportHeight();
1861				refreshHeroMeasurements();
1862				updateScrollAnimations();
1863			}, 220);
1864
1865			return;
1866		}
1867
1868		scrollAnimationResizeTimer = window.setTimeout(refreshScrollAnimationLayout, 150);
1869	});
1870
1871	window.addEventListener('orientationchange', function () {
1872		window.clearTimeout(scrollAnimationResizeTimer);
1873		scrollAnimationResizeTimer = window.setTimeout(refreshScrollAnimationLayout, 350);
1874	});
1875});

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.