PageSourceSearch

https://www.ticketfairy.com/resources/js/landing-cta.js?v=11

js ticketfairy.com collected 2026-09-24 09:10:26 UTC 28,598 bytes, 655 lines download raw bytes

1/* Landing-page CTA utilities (vanilla JS — no jQuery dependency, safe to
2 * load deferred before jQuery).
3 *
4 * 1. Signup links carry permitted campaign parameters and an opaque consent
5 *    handoff. Meta click identifiers stay in the consent-bound server receipt,
6 *    rather than being copied into signup links or booking forms.
7 * 2. CTA click events: pushes a first-party `cta_click` dataLayer event with the CTA
8 *    type (signup / demo / live_demo / video) and page position (hero,
9 *    sticky, header, section id). Google Consent Mode determines whether
10 *    Google receives full or cookieless measurement.
11 * 3. Scroll depth: pushes the equivalent first-party event once per threshold
12 *    (25 / 50 / 75 / 100%).
13 *
14 * GTM/GA4 wiring for these events is documented in
15 * docs/LANDING_CTA_TRACKING.md.
16 */
17(function () {
18    'use strict';
19
20    if (window.__ttfLandingCtaInitialized) return;
21    window.__ttfLandingCtaInitialized = true;
22
23    /* Meta identifiers travel through the server-side consent receipt. Signup
24     * URLs carry only its opaque handoff and the existing campaign parameters. */
25    var PASS_PARAMS = [
26        'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
27        'gclid', 'gbraid', 'wbraid', 'msclkid', 'ttclid', 'irclickid'
28    ];
29    var PERSISTED_PARAMS = [
30        'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
31        'gclid', 'gbraid', 'wbraid', 'fbclid', '_fbc'
32    ];
33    /* The Meta click cookies are refreshed by the consent-gated pixel on every
34     * page load, so they are never the stale value the Google suppression below
35     * exists to keep out, and they must survive a page that carries a Google
36     * click of its own. */
37    var META_PARAMS = ['fbclid', '_fbc'];
38    var metaClickWithdrawn = false;
39    var capturedMetaFbc = null;
40    /* Every Meta cookie this browser may hold, including the durable browser id
41     * that is never put in a URL. Withdrawing permission erases all of them. */
42    var META_COOKIES = ['fbclid', '_fbc', '_fbp'];
43    var CONSENT_HANDOFF_PARAM = 'ttf_consent_handoff';
44    var CONSENT_REFERENCE_KEY = 'ttfAcquisitionAdvertisingConsent';
45    var consentHandoffToken = null;
46    var consentHandoffReference = null;
47    var consentHandoffPromise = null;
48    var consentHandoffGeneration = 0;
49    var pendingConsentRevocations = [];
50    var consentRevocationsInFlight = {};
51    var consentRevocationRetryTimer = null;
52    var consentRevocationRetryDelay = 5000;
53
54    function storedConsentReferences() {
55        try {
56            var value = window.localStorage.getItem(CONSENT_REFERENCE_KEY);
57            if (/^[a-f0-9]{64}$/.test(value || '')) return [value];
58            var parsed = JSON.parse(value || '[]');
59            return Array.isArray(parsed) ? parsed.filter(function (reference, index) {
60                return /^[a-f0-9]{64}$/.test(reference || '')
61                    && parsed.indexOf(reference) === index;
62            }) : [];
63        } catch (e) {
64            return [];
65        }
66    }
67
68    function storeConsentReferences(values) {
69        try {
70            if (values.length) {
71                window.localStorage.setItem(CONSENT_REFERENCE_KEY, JSON.stringify(values));
72            } else {
73                window.localStorage.removeItem(CONSENT_REFERENCE_KEY);
74            }
75        } catch (e) {}
76    }
77
78    /* Keep the handle that withdraws this grant, and say whether it stuck.
79     *
80     * The write is read back rather than assumed. Where storage is blocked --
81     * private browsing, a browser set to refuse site data -- the write throws
82     * and is swallowed, and only the in-memory reference is left. That survives
83     * this page and nothing more, so after a reload a later withdrawal has no
84     * handle for a grant the server still honours, and a conversion for a click
85     * the person has withdrawn could still be reported. The caller uses the
86     * answer to give the grant up instead. */
87    function retainConsentReference(value) {
88        if (!/^[a-f0-9]{64}$/.test(value || '')) return false;
89        var references = storedConsentReferences();
90        if (references.indexOf(value) === -1) references.push(value);
91        storeConsentReferences(references);
92
93        return storedConsentReferences().indexOf(value) !== -1;
94    }
95
96    function forgetConsentReference(value) {
97        storeConsentReferences(storedConsentReferences().filter(function (reference) {
98            return reference !== value;
99        }));
100    }
101
102    function advertisingAllowed() {
103        return window.__ttfAdvertisingAllowed === true;
104    }
105
106    /* Delete one persisted click cookie. The value may have been written by our
107     * server on the shared registrable domain or by Meta's own pixel at the host
108     * level, so both are cleared: a cookie left behind would let a later grant
109     * mint a fresh permission for a click the person has already withdrawn. */
110    function eraseCookie(name) {
111        var past = '; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
112        var host = String(window.location.hostname || '');
113        var labels = host.split('.');
114        try {
115            /* Host-only first, then every parent suffix. Guessing the registrable
116             * domain as the last two labels is wrong on ticketfairy.co.uk and
117             * ticketfairy.com.au, where that yields a public suffix the browser
118             * refuses, leaving the cookie in place. Walking every suffix needs no
119             * public-suffix list: the browser silently ignores the writes it will
120             * not accept, and the one that matches how the cookie was written
121             * succeeds. */
122            document.cookie = name + '=' + past;
123            for (var i = 0; i < labels.length - 1; i++) {
124                var domain = labels.slice(i).join('.');
125                document.cookie = name + '=' + past + '; domain=' + domain;
126                document.cookie = name + '=' + past + '; domain=.' + domain;
127            }
128        } catch (e) {
129            /* A browser refusing the write leaves the cookie to expire on its own. */
130        }
131    }
132
133    function readCookie(name) {
134        var prefix = name + '=';
135        var cookies = String(document.cookie || '').split(';');
136        for (var i = 0; i < cookies.length; i++) {
137            var cookie = cookies[i].trim();
138            if (cookie.indexOf(prefix) !== 0) continue;
139            try {
140                var value = decodeURIComponent(cookie.slice(prefix.length));
141                return value
142                    && value.length <= 512
143                    && !/[\u0000-\u001f\u007f]/.test(value)
144                    ? value
145                    : null;
146            } catch (e) {
147                return null;
148            }
149        }
150        return null;
151    }
152
153    function revokeConsentIdentifier(identifier) {
154        if (!/^[a-f0-9]{64}$/.test(identifier || '') || !window.fetch) {
155            return Promise.resolve(false);
156        }
157        return window.fetch('/api/v1/acquisition/consent-handoff/revoke', {
158            method: 'POST',
159            credentials: 'same-origin',
160            keepalive: true,
161            headers: { 'Content-Type': 'application/json' },
162            body: JSON.stringify({
163                advertisingConsentReference: identifier
164            })
165        }).then(function (response) {
166            return response.ok;
167        }).catch(function () {
168            return false;
169        });
170    }
171
172    function scheduleConsentRevocationRetry() {
173        if (consentRevocationRetryTimer || pendingConsentRevocations.length === 0) return;
174        consentRevocationRetryTimer = window.setTimeout(function () {
175            consentRevocationRetryTimer = null;
176            processPendingConsentRevocations();
177        }, consentRevocationRetryDelay);
178        consentRevocationRetryDelay = Math.min(consentRevocationRetryDelay * 2, 60000);
179    }
180
181    function processPendingConsentRevocations() {
182        pendingConsentRevocations.slice().forEach(function (identifier) {
183            if (consentRevocationsInFlight[identifier]) return;
184            consentRevocationsInFlight[identifier] = true;
185            revokeConsentIdentifier(identifier).then(function (revoked) {
186                delete consentRevocationsInFlight[identifier];
187                if (!revoked) {
188                    scheduleConsentRevocationRetry();
189                    return;
190                }
191                pendingConsentRevocations = pendingConsentRevocations.filter(
192                    function (pendingIdentifier) {
193                        return pendingIdentifier !== identifier;
194                    }
195                );
196                forgetConsentReference(identifier);
197                if (pendingConsentRevocations.length === 0) {
198                    consentRevocationRetryDelay = 5000;
199                }
200            });
201        });
202    }
203
204    function revokeAllConsentIdentifiers() {
205        var identifiers = storedConsentReferences();
206        identifiers.push(consentHandoffReference, consentHandoffToken);
207        identifiers.forEach(function (identifier) {
208            if (
209                /^[a-f0-9]{64}$/.test(identifier || '')
210                && pendingConsentRevocations.indexOf(identifier) === -1
211            ) {
212                pendingConsentRevocations.push(identifier);
213            }
214        });
215        processPendingConsentRevocations();
216    }
217
218    function collectParams() {
219        if (!advertisingAllowed()) return null;
220
221        var out = {};
222        var found = false;
223        try {
224            var search = new URLSearchParams(window.location.search);
225            var hasCurrentGoogleClick = Boolean(
226                search.get('gclid') || search.get('gbraid') || search.get('wbraid')
227            );
228            var captureParams = PASS_PARAMS.concat(META_PARAMS);
229            for (var i = 0; i < captureParams.length; i++) {
230                if (metaClickWithdrawn && META_PARAMS.indexOf(captureParams[i]) !== -1) continue;
231                var v = search.get(captureParams[i]);
232                var suppressPersisted = hasCurrentGoogleClick
233                    && META_PARAMS.indexOf(captureParams[i]) === -1;
234                if (
235                    !v
236                    && !suppressPersisted
237                    && PERSISTED_PARAMS.indexOf(captureParams[i]) !== -1
238                ) {
239                    v = readCookie(captureParams[i]);
240                }
241                if (v) {
242                    out[captureParams[i]] = v;
243                    found = true;
244                }
245            }
246            if (out.fbclid && /^[A-Za-z0-9_-]{8,512}$/.test(out.fbclid)) {
247                var fbcParts = String(out._fbc || '').split('.');
248                if (fbcParts.slice(3).join('.') !== out.fbclid) {
249                    if (!capturedMetaFbc || capturedMetaFbc.slice(capturedMetaFbc.lastIndexOf('.') + 1) !== out.fbclid) {
250                        capturedMetaFbc = 'fb.1.' + Date.now() + '.' + out.fbclid;
251                    }
252                    out._fbc = capturedMetaFbc;
253                }
254            }
255            var body = document.body;
256            var landingPageId = body ? body.getAttribute('data-google-ads-landing-page-id') : '';
257            if (/^[1-9][0-9]{0,10}$/.test(landingPageId || '')) {
258                out.gads_lp_id = landingPageId;
259                found = true;
260            } else if (!hasCurrentGoogleClick) {
261                var persistedLandingPageId = readCookie('gads_lp_id');
262                if (/^[1-9][0-9]{0,10}$/.test(persistedLandingPageId || '')) {
263                    out.gads_lp_id = persistedLandingPageId;
264                    found = true;
265                }
266            }
267        } catch (e) {
268            return null; // URLSearchParams unsupported — leave links untouched
269        }
270        return found ? out : null;
271    }
272
273    function decorateSignupLinks() {
274        var params = collectParams();
275        var links = document.querySelectorAll(
276            'a[href^="https://manage.ticketfairy.com/welcome"], ' +
277            'a[href^="https://manage.ticketfairy.com/registration"]'
278        );
279        for (var i = 0; i < links.length; i++) {
280            try {
281                var url = new URL(links[i].href);
282                META_PARAMS.forEach(function (name) { url.searchParams.delete(name); });
283
284                if (!params) {
285                    for (var p = 0; p < PASS_PARAMS.length; p++) {
286                        url.searchParams.delete(PASS_PARAMS[p]);
287                    }
288                    url.searchParams.delete('gads_lp_id');
289                    url.searchParams.delete(CONSENT_HANDOFF_PARAM);
290                } else {
291                    for (var key in params) {
292                        if (Object.prototype.hasOwnProperty.call(params, key) && META_PARAMS.indexOf(key) === -1 && !url.searchParams.has(key)) {
293                            url.searchParams.set(key, params[key]);
294                        }
295                    }
296                    if (consentHandoffToken) {
297                        url.searchParams.set(CONSENT_HANDOFF_PARAM, consentHandoffToken);
298                        META_PARAMS.forEach(function (name) { url.searchParams.delete(name); });
299                    }
300                }
301                links[i].href = url.toString();
302            } catch (e) {
303                // Malformed href or no URL support — leave this link as-is.
304            }
305        }
306
307        decorateSignupForms(params);
308    }
309
310    /* A specialist landing page's email field posts to our own host, which files the address and
311     * then sends the visitor to signup. The same parameters the links carry
312     * travel as hidden fields, so that hop keeps the attribution a link would
313     * have handed over directly. */
314    function decorateSignupForms(params) {
315        var forms = document.querySelectorAll('form.js-signup-form');
316
317        for (var i = 0; i < forms.length; i++) {
318            var carried = {};
319
320            if (params) {
321                for (var key in params) {
322                    if (Object.prototype.hasOwnProperty.call(params, key) && META_PARAMS.indexOf(key) === -1) {
323                        carried[key] = params[key];
324                    }
325                }
326                if (consentHandoffToken) {
327                    carried[CONSENT_HANDOFF_PARAM] = consentHandoffToken;
328                }
329            }
330
331            setCarriedFields(forms[i], carried);
332        }
333    }
334
335    /* One hidden field per carried parameter, replaced rather than added to, so
336     * a consent choice made after the page loaded cannot leave a stale value
337     * behind. */
338    function setCarriedFields(form, carried) {
339        var existing = form.querySelectorAll('input[data-cta-carried]');
340
341        for (var i = 0; i < existing.length; i++) {
342            existing[i].parentNode.removeChild(existing[i]);
343        }
344
345        for (var key in carried) {
346            if (!Object.prototype.hasOwnProperty.call(carried, key)) continue;
347            var field = document.createElement('input');
348            field.type = 'hidden';
349            field.name = key;
350            field.value = carried[key];
351            field.setAttribute('data-cta-carried', '');
352            form.appendChild(field);
353        }
354    }
355
356    function syncConsentHandoff() {
357        var generation = ++consentHandoffGeneration;
358
359        if (!advertisingAllowed()) {
360            if (window.__ttfAdvertisingChoice !== 'denied') {
361                decorateSignupLinks();
362                return null;
363            }
364            metaClickWithdrawn = true;
365            revokeAllConsentIdentifiers();
366            /* Revoking the server reference stops the reporting. Erasing the
367             * click cookies is the other half: without it, this browser still
368             * carries the click, and the next time the person allows
369             * advertising a brand-new permission would be minted for the very
370             * click they withdrew. */
371            for (var m = 0; m < META_COOKIES.length; m++) {
372                eraseCookie(META_COOKIES[m]);
373            }
374            consentHandoffToken = null;
375            consentHandoffReference = null;
376            consentHandoffPromise = null;
377            window.__ttfAcquisitionConsentHandoffPromise = null;
378            window.__ttfAcquisitionConsentReference = null;
379            window.__ttfAcquisitionTracking = null;
380            decorateSignupLinks();
381            return null;
382        }
383
384        var tracking = collectParams();
385        /* A Meta click needs the same durable, revocable permission a Google
386         * click does: the demo booking, the signup and the activation are all
387         * reported from the server days later, and each one rechecks it. */
388        var hasPaidClick = tracking
389            && (tracking.gclid || tracking.gbraid || tracking.wbraid
390                || tracking.fbclid || tracking._fbc);
391        if (!tracking || !hasPaidClick || !window.fetch) {
392            return null;
393        }
394        window.__ttfAcquisitionTracking = tracking;
395
396        function requestHandoff(bundle) {
397            return window.fetch('/api/v1/acquisition/consent-handoff', {
398                method: 'POST',
399                credentials: 'same-origin',
400                headers: { 'Content-Type': 'application/json' },
401                body: JSON.stringify(bundle)
402            }).then(function (response) {
403                return response.ok ? response.json() : null;
404            });
405        }
406        consentHandoffPromise = requestHandoff(tracking).then(function (result) {
407            if (result || !(tracking.gclid || tracking.gbraid || tracking.wbraid)
408                || !(tracking.fbclid || tracking._fbc)
409                || generation !== consentHandoffGeneration || !advertisingAllowed()) return result;
410            /* A failed Meta receipt must not disable the existing Google path.
411             * Retry only that network's exact submitted bundle once. */
412            var googleTracking = {};
413            Object.keys(tracking).forEach(function (key) {
414                if (META_PARAMS.indexOf(key) === -1) googleTracking[key] = tracking[key];
415            });
416            tracking = googleTracking;
417            window.__ttfAcquisitionTracking = tracking;
418            return requestHandoff(tracking);
419        }).then(function (result) {
420            var token = result && result.token;
421            var consentReference = result && result.consent_reference;
422            if (generation !== consentHandoffGeneration || !advertisingAllowed()) {
423                revokeConsentIdentifier(consentReference || token);
424                return null;
425            }
426            consentHandoffToken = /^[a-f0-9]{64}$/.test(token || '') ? token : null;
427            consentHandoffReference = /^[a-f0-9]{64}$/.test(consentReference || '')
428                ? consentReference
429                : null;
430            if (consentHandoffReference && !retainConsentReference(consentHandoffReference)) {
431                /* A grant we cannot withdraw later is worse than no grant. Hand
432                 * it straight back while it is still in memory and carry on
433                 * without one: not reporting a conversion is the acceptable
434                 * failure here, reporting one after a withdrawal is not.
435                 *
436                 * Through the pending queue rather than a single call. A one
437                 * shot that is refused or aborted would leave the grant alive
438                 * on the server with the handle already dropped from memory,
439                 * which is the failure this is here to close. The queue holds
440                 * its own copy and retries with backoff. */
441                if (pendingConsentRevocations.indexOf(consentHandoffReference) === -1) {
442                    pendingConsentRevocations.push(consentHandoffReference);
443                }
444                processPendingConsentRevocations();
445                consentHandoffToken = null;
446                consentHandoffReference = null;
447            }
448            window.__ttfAcquisitionConsentReference = consentHandoffReference;
449            decorateSignupLinks();
450            return consentHandoffToken;
451        }).catch(function () {
452            if (generation === consentHandoffGeneration) {
453                consentHandoffToken = null;
454                consentHandoffReference = null;
455                window.__ttfAcquisitionConsentReference = null;
456            }
457            return null;
458        });
459        window.__ttfAcquisitionConsentHandoffPromise = consentHandoffPromise;
460
461        return consentHandoffPromise;
462    }
463
464    function preserveHandoffBeforeSignupNavigation() {
465        document.addEventListener('click', function (event) {
466            var target = event.target;
467            var link = target && target.closest ? target.closest(
468                'a[href*="manage.ticketfairy.com/welcome"], ' +
469                'a[href*="manage.ticketfairy.com/registration"]'
470            ) : null;
471
472            if (!link || !advertisingAllowed() || !consentHandoffPromise) return;
473
474            var opensNewContext = link.target === '_blank'
475                || event.metaKey
476                || event.ctrlKey
477                || event.shiftKey;
478            var pendingWindow = null;
479            if (opensNewContext) {
480                try {
481                    pendingWindow = window.open('about:blank', '_blank');
482                    if (pendingWindow) pendingWindow.opener = null;
483                } catch (e) {
484                    pendingWindow = null;
485                }
486                // Preserve the browser's native navigation when pop-ups are blocked.
487                if (!pendingWindow) return;
488            }
489
490            event.preventDefault();
491            var navigated = false;
492            var navigate = function () {
493                if (navigated) return;
494                navigated = true;
495                if (pendingWindow) {
496                    if (!pendingWindow.closed) pendingWindow.location.href = link.href;
497                    return;
498                }
499                window.location.href = link.href;
500            };
501            consentHandoffPromise.then(navigate, navigate);
502            window.setTimeout(navigate, 1500);
503        }, true);
504
505        preserveHandoffBeforeSignupSubmit();
506    }
507
508    /* A specialist landing page's email form posts as soon as it is submitted, which can be
509     * before the consent handoff has been minted. Hold the submission for the
510     * same moment the link path waits for, then send it with the token in
511     * place, and never longer than the same 1.5 seconds. */
512    function preserveHandoffBeforeSignupSubmit() {
513        document.addEventListener('submit', function (event) {
514            var form = event.target;
515
516            if (!form || !form.classList || !form.classList.contains('js-signup-form')) return;
517            if (form.getAttribute('data-cta-handoff-ready')) return;
518            if (!advertisingAllowed() || !consentHandoffPromise || consentHandoffToken) return;
519
520            event.preventDefault();
521            var submitted = false;
522            var submit = function () {
523                if (submitted) return;
524                submitted = true;
525                decorateSignupForms(collectParams());
526                form.setAttribute('data-cta-handoff-ready', '1');
527                form.submit();
528            };
529            consentHandoffPromise.then(submit, submit);
530            window.setTimeout(submit, 1500);
531        }, true);
532    }
533
534    function ctaPosition(el) {
535        var explicit = el.getAttribute('data-cta-position');
536        if (explicit) return explicit;
537        if (!el.closest) return 'page';
538        var scope = el.closest('.sticky-ctas, header, section[id], section, [id]');
539        if (!scope) return 'page';
540        if (scope.className && String(scope.className).indexOf('sticky-ctas') !== -1) return 'sticky';
541        if (scope.tagName === 'HEADER') return 'header';
542        return scope.id || 'page';
543    }
544
545    function trackCtaClicks() {
546        document.addEventListener('click', function (e) {
547            var target = e.target;
548            if (!target || !target.closest) return;
549            // a[href$="#apply"] covers pages whose demo CTAs scroll to an
550            // on-page application form instead of opening the modal (v9).
551            // capital.ticketfairy.com is the funding application, which is the
552            // conversion /capital exists to drive — it is not a signup.
553            var el = target.closest(
554                'a[href*="manage.ticketfairy.com/welcome"], ' +
555                'a[href*="manage.ticketfairy.com/registration"], ' +
556                'a[href*="capital.ticketfairy.com"], ' +
557                '.open-modal, .js-watch-video, .js-open-dashboard-demo, ' +
558                '.js-signup-submit, a[href$="#apply"]'
559            );
560            if (!el) return;
561            var type = 'signup';
562            if (el.getAttribute('href') && el.getAttribute('href').indexOf('capital.ticketfairy.com') !== -1) {
563                type = 'capital_application';
564            } else if (el.classList.contains('open-modal')) {
565                type = 'demo';
566            } else if (el.classList.contains('js-watch-video')) {
567                type = 'video';
568            } else if (el.classList.contains('js-open-dashboard-demo')) {
569                type = 'live_demo';
570            } else if (el.getAttribute('href') && /#apply$/.test(el.getAttribute('href'))) {
571                type = 'demo';
572            }
573            var position = ctaPosition(el);
574            window.dataLayer = window.dataLayer || [];
575            window.dataLayer.push({
576                event: 'cta_click',
577                cta_type: type,
578                cta_position: position
579            });
580
581            // A funding application is a Meta Lead conversion, and campaign
582            // optimisation reads it. It carries the visitor's identifiers, so
583            // it is emitted only while the advertising choice is granted —
584            // unlike the first-party event above, which consent mode keeps.
585            if (type === 'capital_application' && advertisingAllowed() && typeof window.fbq === 'function') {
586                window.fbq('track', 'Lead', {
587                    content_name: 'Ticket Fairy Capital application',
588                    content_category: position
589                });
590            }
591        }, true);
592    }
593
594    function trackScrollDepth() {
595        var thresholds = [25, 50, 75, 100];
596        var fired = 0;
597        var ticking = false;
598
599        function measure() {
600            ticking = false;
601            var doc = document.documentElement;
602            var scrollable = doc.scrollHeight - window.innerHeight;
603            if (scrollable <= 0) return;
604            // The +2px slack absorbs sub-pixel gaps between scrollTop,
605            // innerHeight and scrollHeight at the very bottom of the page,
606            // which otherwise leave pct just under 100 and the final
607            // milestone unfired.
608            var pct = (((window.pageYOffset || doc.scrollTop || 0) + 2) / scrollable) * 100;
609
610            while (fired < thresholds.length && pct >= thresholds[fired]) {
611                window.dataLayer = window.dataLayer || [];
612                window.dataLayer.push({
613                    event: 'scroll_depth',
614                    percent_scrolled: thresholds[fired]
615                });
616                fired++;
617            }
618            if (fired >= thresholds.length) {
619                window.removeEventListener('scroll', onScroll);
620                window.removeEventListener('pageshow', onScroll);
621            }
622        }
623
624        function onScroll() {
625            if (ticking) return;
626            ticking = true;
627            (window.requestAnimationFrame || window.setTimeout)(measure);
628        }
629
630        window.addEventListener('scroll', onScroll, { passive: true });
631        // Browsers restore the scroll offset on reload / back-forward cache
632        // without emitting a scroll event — measure once at init and again
633        // on pageshow so milestones below a restored position still fire.
634        measure();
635        window.addEventListener('pageshow', onScroll);
636    }
637
638    function init() {
639        decorateSignupLinks();
640        syncConsentHandoff();
641        window.addEventListener('ttf:advertising-choice', function () {
642            decorateSignupLinks();
643            syncConsentHandoff();
644        });
645        preserveHandoffBeforeSignupNavigation();
646        trackCtaClicks();
647        trackScrollDepth();
648    }
649
650    if (document.readyState === 'loading') {
651        document.addEventListener('DOMContentLoaded', init);
652    } else {
653        init();
654    }
655})();

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.