PageSourceSearch

https://cdn.fantasyfootballscout.co.uk/wp-content/themes/football-…/js/scout-interstitial.js?ver=1789770862

js fantasyfootballscout.co.uk collected 2026-09-24 09:19:07 UTC 29,554 bytes, 873 lines download raw bytes

1/**
2 * Daily full-page membership interstitial.
3 *
4 * Page one arms a first-party cookie. The next eligible page is covered in the
5 * head, before body content can paint, and the offer is then capped for 24h.
6 */
7(function () {
8  "use strict";
9
10  const config = window.ffsScoutInterstitial || {};
11  const query = new URLSearchParams(window.location.search);
12  const previewValue = query.get("scout_interstitial");
13  const testerParam = query.get("scout_interstitial_test");
14  const testerCookieName = "ffs_scout_interstitial_test";
15  const testerOptIn = testerParam === "1";
16  const testerOptOut = testerParam === "0";
17
18  if (testerOptIn) {
19    setTesterCookie(true);
20  } else if (testerOptOut) {
21    setTesterCookie(false);
22  }
23
24  const testerMode =
25    !testerOptOut &&
26    (testerOptIn || safeCookieGet(testerCookieName) === "1");
27  const previewMode =
28    testerMode &&
29    (testerOptIn ||
30      previewValue === "1" ||
31      previewValue === "annual" ||
32      previewValue === "monthly" ||
33      query.get("forced_popup") === "1" ||
34      query.get("force_popup") === "1");
35  const dailyStateCookieName = "ffs_scout_interstitial_daily_state";
36  const dailyStateSeconds = 24 * 60 * 60;
37  const dailyStateMilliseconds = dailyStateSeconds * 1000;
38  const dailyFallbackStorageKey = "ffsScoutInterstitialDailyState";
39  const sessionKey = "ffsScoutInterstitialState";
40  const sessionExpiryKey = "ffsScoutInterstitialStateExpiresAt";
41  const oldSessionKey = "ffsForcedPopupShown";
42  const memberStorageKeys = [
43    "ffsScoutInterstitialWasMemberAt",
44    "ffsForcedPopupWasMemberAt",
45    "wasMemberAt",
46  ];
47  const memberCookieName = "ffs_scout_interstitial_was_member_at";
48  const memberCookieSeconds = 400 * 24 * 60 * 60;
49  const interstitialId = "ffs-scout-interstitial";
50  const flashClickStorageKey = "ffsFlashSaleClickId";
51  const flashClickCookieName = "ffs_flash_sale_click";
52  const excludedPaths = [
53    "/register",
54    "/upgrade",
55    "/benefits",
56    "/account",
57    "/app-account",
58    "/app-login",
59    "/my-account",
60    "/login",
61    "/sign-in",
62  ];
63  let failOpenTimer = null;
64  let viewportResizeHandler = null;
65  let viewportScrollHandler = null;
66  let bodyObserver = null;
67  let titleResizeObserver = null;
68
69  if (config.isLoggedIn && config.isMember) {
70    rememberPaidMember();
71    setDailyState("shown");
72    return;
73  }
74
75  // A remembered paid member stays excluded after logout. Only clear that
76  // browser marker once the site can confirm they have logged in as a
77  // non-paying user; this avoids showing the acquisition offer between
78  // sessions to somebody whose paid account is still active.
79  if (config.isLoggedIn) {
80    forgetPaidMember();
81  }
82
83  if (!config.enabled && !testerMode) {
84    return;
85  }
86
87  // An explicit tester preview must still be usable in a browser that once
88  // belonged to a paid member. Normal visits retain the remembered-member
89  // suppression below.
90  if (!previewMode && wasPaidMemberBeforeLogout()) {
91    setDailyState("shown");
92    return;
93  }
94
95  if (!previewMode && safeSessionGet(oldSessionKey)) {
96    setDailyState("shown");
97    return;
98  }
99
100  const dailyState = getDailyState();
101
102  if (!previewMode && dailyState === "shown") {
103    return;
104  }
105
106  // The first page view only arms the daily state. This deliberately happens
107  // even on an excluded route so the first later eligible page can show it.
108  if (!previewMode && dailyState !== "armed") {
109    setDailyState("armed");
110    return;
111  }
112
113  if (!previewMode && isExcludedRoute(window.location.pathname)) {
114    return;
115  }
116
117  // This runs while the browser is still parsing <head>, preventing a flash of
118  // the destination page. A fail-open timer ensures content can never remain
119  // hidden if another script or malformed page stops the component rendering.
120  document.documentElement.classList.add("ffs-scout-interstitial-pending");
121  if (config.theme === "ucl") {
122    document.documentElement.classList.add("ffs-ucl-interstitial");
123  }
124  failOpenTimer = window.setTimeout(failOpen, 4000);
125  whenBodyExists(showInterstitial);
126
127  function getDailyState() {
128    const cookieState = safeCookieGet(dailyStateCookieName);
129    const fallbackState = getDailyFallbackState();
130    let legacySessionState = safeSessionGet(sessionKey);
131    const sessionExpiry = parseInt(safeSessionGet(sessionExpiryKey), 10);
132
133    if (
134      legacySessionState &&
135      Number.isFinite(sessionExpiry) &&
136      sessionExpiry <= Date.now()
137    ) {
138      safeSessionRemove(sessionKey);
139      safeSessionRemove(sessionExpiryKey);
140      legacySessionState = null;
141    }
142
143    const storedState =
144      cookieState === "armed" || cookieState === "shown"
145        ? cookieState
146        : fallbackState;
147
148    // Smooth deployment across pages that may briefly use different cached
149    // script versions. An old script can mark the session as shown after the
150    // new script armed the cookie; promote that result to the daily cap.
151    if (legacySessionState === "shown" && storedState !== "shown") {
152      setDailyState("shown");
153      return "shown";
154    }
155
156    if (storedState === "armed" || storedState === "shown") {
157      return storedState;
158    }
159
160    // Migrate an existing session state once so deploying this change cannot
161    // immediately show a second interstitial to somebody who already saw it.
162    if (legacySessionState === "armed" || legacySessionState === "shown") {
163      setDailyState(legacySessionState);
164      return legacySessionState;
165    }
166
167    return null;
168  }
169
170  function setDailyState(state) {
171    const secure = window.location.protocol === "https:" ? "; Secure" : "";
172    const expiresAt = Date.now() + dailyStateMilliseconds;
173    document.cookie =
174      encodeURIComponent(dailyStateCookieName) +
175      "=" +
176      encodeURIComponent(state) +
177      "; Max-Age=" +
178      dailyStateSeconds +
179      "; Path=/; SameSite=Lax" +
180      secure;
181
182    safeLocalSet(
183      dailyFallbackStorageKey,
184      JSON.stringify({ state: state, expiresAt: expiresAt }),
185    );
186
187    // Retain a time-bounded same-session marker for pages temporarily running
188    // the previous cached script revision. The explicit expiry prevents a tab
189    // left open for several days from suppressing every subsequent daily view.
190    safeSessionSet(sessionKey, state);
191    safeSessionSet(sessionExpiryKey, expiresAt.toString());
192  }
193
194  function getDailyFallbackState() {
195    const value = safeLocalGet(dailyFallbackStorageKey);
196    if (!value) {
197      return null;
198    }
199
200    try {
201      const stored = JSON.parse(value);
202      if (
203        (stored.state === "armed" || stored.state === "shown") &&
204        Number.isFinite(stored.expiresAt) &&
205        stored.expiresAt > Date.now()
206      ) {
207        return stored.state;
208      }
209    } catch (error) {
210      // Invalid legacy/browser-managed data is treated as expired.
211    }
212
213    safeLocalRemove(dailyFallbackStorageKey);
214    return null;
215  }
216
217  function safeSessionGet(key) {
218    try {
219      return window.sessionStorage.getItem(key);
220    } catch (error) {
221      return safeCookieGet(key);
222    }
223  }
224
225  function safeCookieGet(key) {
226    const cookieName = encodeURIComponent(key) + "=";
227    const cookies = document.cookie ? document.cookie.split(";") : [];
228
229    for (let index = 0; index < cookies.length; index += 1) {
230      const cookie = cookies[index].trim();
231      if (cookie.indexOf(cookieName) === 0) {
232        return decodeURIComponent(cookie.substring(cookieName.length));
233      }
234    }
235
236    return null;
237  }
238
239  function setTesterCookie(enabled) {
240    const secure = window.location.protocol === "https:" ? "; Secure" : "";
241    document.cookie = enabled
242      ? `${encodeURIComponent(testerCookieName)}=1; Max-Age=604800; Path=/; SameSite=Lax${secure}`
243      : `${encodeURIComponent(testerCookieName)}=; Max-Age=0; Path=/; SameSite=Lax${secure}`;
244  }
245
246  function safeSessionSet(key, value) {
247    try {
248      window.sessionStorage.setItem(key, value);
249      return;
250    } catch (error) {
251      document.cookie =
252        encodeURIComponent(key) +
253        "=" +
254        encodeURIComponent(value) +
255        "; path=/; SameSite=Lax";
256    }
257  }
258
259  function safeSessionRemove(key) {
260    try {
261      window.sessionStorage.removeItem(key);
262    } catch (error) {
263      document.cookie =
264        encodeURIComponent(key) +
265        "=; Max-Age=0; Path=/; SameSite=Lax";
266    }
267  }
268
269  function safeLocalGet(key) {
270    try {
271      return window.localStorage.getItem(key);
272    } catch (error) {
273      return null;
274    }
275  }
276
277  function safeLocalSet(key, value) {
278    try {
279      window.localStorage.setItem(key, value);
280    } catch (error) {
281      // Privacy modes can block localStorage. The companion cookie below
282      // still preserves former-member suppression where cookies are allowed.
283    }
284  }
285
286  function safeLocalRemove(key) {
287    try {
288      window.localStorage.removeItem(key);
289    } catch (error) {
290      // Nothing else to clear when localStorage is unavailable.
291    }
292  }
293
294  function setPaidMemberCookie(timestamp) {
295    const secure = window.location.protocol === "https:" ? "; Secure" : "";
296    document.cookie =
297      encodeURIComponent(memberCookieName) +
298      "=" +
299      encodeURIComponent(timestamp) +
300      "; Max-Age=" +
301      memberCookieSeconds +
302      "; Path=/; SameSite=Lax" +
303      secure;
304  }
305
306  function clearPaidMemberCookie() {
307    const secure = window.location.protocol === "https:" ? "; Secure" : "";
308    document.cookie =
309      encodeURIComponent(memberCookieName) +
310      "=; Max-Age=0; Path=/; SameSite=Lax" +
311      secure;
312  }
313
314  function rememberPaidMember() {
315    const timestamp = Date.now().toString();
316    memberStorageKeys.forEach(function (key) {
317      safeLocalSet(key, timestamp);
318    });
319    setPaidMemberCookie(timestamp);
320  }
321
322  function forgetPaidMember() {
323    memberStorageKeys.forEach(function (key) {
324      safeLocalRemove(key);
325    });
326    clearPaidMemberCookie();
327  }
328
329  function wasPaidMemberBeforeLogout() {
330    return (
331      memberStorageKeys.some(function (key) {
332        return safeLocalGet(key) !== null;
333      }) || safeCookieGet(memberCookieName) !== null
334    );
335  }
336
337  function isExcludedRoute(pathname) {
338    return excludedPaths.some(function (path) {
339      return pathname.indexOf(path) === 0;
340    });
341  }
342
343  function whenBodyExists(callback) {
344    if (document.body) {
345      callback();
346      return;
347    }
348
349    bodyObserver = new MutationObserver(function () {
350      if (!document.body) {
351        return;
352      }
353
354      bodyObserver.disconnect();
355      bodyObserver = null;
356      callback();
357    });
358    bodyObserver.observe(document.documentElement, {
359      childList: true,
360      subtree: true,
361    });
362
363    document.addEventListener(
364      "DOMContentLoaded",
365      function () {
366        if (document.body && !document.getElementById(interstitialId)) {
367          if (bodyObserver) {
368            bodyObserver.disconnect();
369            bodyObserver = null;
370          }
371          callback();
372        }
373      },
374      { once: true },
375    );
376  }
377
378  function showInterstitial() {
379    if (!document.body || document.getElementById(interstitialId)) {
380      failOpen();
381      return;
382    }
383
384    const audience = getAudience();
385    const offer = getOffer(audience);
386    const clickId = getOrCreateFlashClickId();
387    const targetUrl = campaignUrl(
388      config.isLoggedIn ? config.upgradeUrl : config.registerUrl,
389      audience,
390      clickId,
391    );
392
393    document.body.insertAdjacentHTML(
394      "afterbegin",
395      interstitialMarkup(audience, offer, targetUrl),
396    );
397
398    const interstitial = document.getElementById(interstitialId);
399    if (!interstitial) {
400      failOpen();
401      return;
402    }
403
404    setDailyState("shown");
405    document.documentElement.classList.remove(
406      "ffs-scout-interstitial-pending",
407    );
408    document.documentElement.classList.add("ffs-scout-interstitial-open");
409    window.clearTimeout(failOpenTimer);
410    failOpenTimer = null;
411
412    const dialog = interstitial.querySelector("[data-interstitial-dialog]");
413    const closeButtons = interstitial.querySelectorAll(
414      "[data-interstitial-dismiss]",
415    );
416    const cta = interstitial.querySelector("[data-interstitial-cta]");
417
418    closeButtons.forEach(function (button) {
419      button.addEventListener("click", function () {
420        trackEvent("dismiss", audience);
421        closeInterstitial();
422      });
423    });
424    cta.addEventListener("click", function () {
425      trackEvent("cta", audience);
426      recordFlashSaleClick(clickId, audience);
427    });
428    document.addEventListener("keydown", handleKeydown);
429    startViewportTracking();
430    startTitleFitting();
431    trackEvent("impression", audience);
432
433    if (dialog) {
434      window.requestAnimationFrame(function () {
435        try {
436          dialog.focus({ preventScroll: true });
437        } catch (error) {
438          dialog.focus();
439        }
440      });
441    }
442  }
443
444  function getAudience() {
445    return config.isLoggedIn ? "free-member" : "guest";
446  }
447
448  function getOffer() {
449    const configuredOffers = config.offers || {};
450    return Object.assign(
451      { currentPrice: "£5", originalPrice: "£10", saving: "£5 off your first month" },
452      configuredOffers.flash || {},
453    );
454  }
455
456  function campaignUrl(url, audience, clickId) {
457    const fallback = config.isLoggedIn
458      ? "/upgrade?sub=monthly&bundle=none&offer=flash-sale-ffs5"
459      : "/register?sub=monthly&bundle=none&offer=flash-sale-ffs5";
460
461    try {
462      const target = new URL(url || fallback, window.location.origin);
463      target.searchParams.set("sub", "monthly");
464      target.searchParams.set("bundle", "none");
465      target.searchParams.set("offer", "flash-sale-ffs5");
466      target.searchParams.set("flash_click", clickId);
467      target.searchParams.set("utm_source", "scout_interstitial");
468      target.searchParams.set("utm_medium", "onsite");
469      target.searchParams.set("utm_campaign", config.campaign || "new-season-flash-sale-ffs5");
470      target.searchParams.set("utm_content", audience);
471      return target.toString();
472    } catch (error) {
473      return fallback;
474    }
475  }
476
477  function interstitialMarkup(audience, offer, targetUrl) {
478    // wp_localize_script serializes top-level booleans as "1" / "".
479    const isTrial = config.freeTrial === true || config.freeTrial === "1";
480    const isUclCampaign = !isTrial && config.theme === "ucl";
481    const creative = isUclCampaign
482      ? {
483          eyebrow: "New Season Sale · UCL Toolkit Included",
484          title: "Win at UCL Fantasy & FPL with Chief Scout",
485          intro:
486            "Our brand-new UCL Fantasy Toolkit is now part of Chief Scout for the 2026/27 season, bringing you projected points, domestic data and smarter squad tools.",
487          benefitsTitle: "Everything you need to Win at UCL Fantasy & FPL",
488          benefitsIntro:
489            "Your UCL toolkit sits alongside the complete Chief Scout FPL offering in one membership.",
490          features: [
491            [
492              "Find Hidden Gems with Domestic Data",
493              "Compare domestic form across every UCL club.",
494            ],
495            [
496              "Plan Ahead with the Fixture Ticker",
497              "Spot the best fixtures across all eight matchdays.",
498            ],
499            [
500              "Build Smarter with the Draft Tool",
501              "Create and optimise your 15-player UCL squad.",
502            ],
503            [
504              "Improve Your Squad with our Team Rater",
505              "Get an instant rating and recommended improvements.",
506            ],
507          ],
508        }
509      : {
510          eyebrow: "New season flash sale · £5 off",
511          title: "Kick Off the Season with Chief Scout for £5",
512          intro:
513            "Unlock the complete Scout toolkit, sharper projections and premium Opta data for half price in your first month.",
514          benefitsTitle: "Everything you need to Win at FPL",
515          benefitsIntro:
516            "Build a stronger squad, make smarter decisions and score more points with the Scout tools trusted by over 360,000 FPL managers, including FPL Harry, FPL Focal and FPL General.",
517          features: [
518            [
519              "Industry-Leading RMT Projections",
520              "See how many points your players are expected to score and use industry leading projections to make smarter team selections.",
521            ],
522            [
523              "Unlimited RMT Draft Ratings & Squad Analysis",
524              "Rate unlimited drafts, identify your squad’s strengths and weaknesses, and make smarter changes before every deadline.",
525            ],
526            [
527              "Personalised Transfer Recommendations",
528              "Get personalised transfer suggestions based on your squad to quickly identify the moves most likely to improve your team.",
529            ],
530            [
531              "The Ultimate FPL Pre-Season Guide",
532              "Prepare for Gameweek 1 with expert player picks, fixture analysis, team reveals and chip strategies all in one place.",
533            ],
534          ],
535        };
536    if (isTrial) {
537      creative.eyebrow = "Chief Scout FREE Trial Until GW6";
538      creative.title = "Save Your FPL Season. Try Chief Scout FREE.";
539      creative.intro = "Beat the international break boredom and plan your FPL comeback with FREE Chief Scout access until the GW6 deadline.";
540    }
541    const priceCopy = isTrial
542      ? "Try Chief Scout FREE until the GW6 deadline, then pay just £5 for your first month."
543      : "your first month<br>then £10/month";
544    const cta = isTrial ? "Start My Free Trial" : "Get Chief Scout for £5";
545    const terms = isTrial
546      ? "Code FFS5 is applied automatically. First paid month £5 after your free trial, then £10/month. Cancel anytime."
547      : "Code FFS5 is applied automatically. First month £5, then £10/month. Cancel anytime.";
548    const themeClass = isUclCampaign
549      ? "ffs-interstitial--ucl"
550      : "ffs-interstitial--scout";
551    const ariaLabel = isUclCampaign
552      ? "Chief Scout UCL Fantasy launch offer"
553      : "Fantasy Football Scout flash sale";
554
555    return `
556      <section id="${interstitialId}" class="ffs-interstitial--monthly ffs-interstitial--flash ${themeClass}" aria-label="${ariaLabel}">
557        <div class="ffs-interstitial__shell">
558          <header class="ffs-interstitial__topbar">
559            <div class="ffs-interstitial__brands">
560              <img class="ffs-interstitial__logo" src="${escapeHtml(
561                (isUclCampaign ? config.uclLogoUrl : config.logoUrl) ||
562                  config.logoUrl ||
563                  "",
564              )}" alt="Fantasy Football Scout">
565              ${
566                isUclCampaign && config.uclCompetitionLogoUrl
567                  ? `<span class="ffs-interstitial__brand-divider" aria-hidden="true"></span>
568                     <img class="ffs-interstitial__ucl-logo" src="${escapeHtml(config.uclCompetitionLogoUrl)}" alt="UEFA Champions League">`
569                  : ""
570              }
571            </div>
572            <button type="button" class="ffs-interstitial__skip" data-interstitial-dismiss aria-label="Dismiss offer and continue to the site">
573              <span class="ffs-interstitial__skip-label">Continue to site</span>
574              ${closeIcon()}
575            </button>
576          </header>
577
578          <div class="ffs-interstitial__panel" role="dialog" aria-modal="true" aria-labelledby="ffs-interstitial-title" data-interstitial-dialog tabindex="-1">
579            <div class="ffs-interstitial__offer">
580              <p class="ffs-interstitial__eyebrow">${escapeHtml(creative.eyebrow)}</p>
581              <h1 class="ffs-interstitial__title" id="ffs-interstitial-title">${escapeHtml(creative.title)}</h1>
582              <p class="ffs-interstitial__intro">${escapeHtml(creative.intro)}</p>
583
584              <div class="ffs-interstitial__price" aria-label="${escapeHtml(
585                isTrial ? "Chief Scout free trial until GW6" : offer.saving,
586              )}">
587                <div class="ffs-interstitial__price-main">
588                  <strong class="ffs-interstitial__price-now">${escapeHtml(
589                    isTrial ? "FREE" : offer.currentPrice,
590                  )}</strong>
591                  <span class="ffs-interstitial__price-was">${escapeHtml(
592                    offer.originalPrice,
593                  )}</span>
594                </div>
595                <p class="ffs-interstitial__price-copy">${priceCopy}</p>
596              </div>
597
598              <div class="ffs-interstitial__actions">
599                <a class="ffs-interstitial__cta" href="${escapeHtml(
600                  targetUrl,
601                )}" data-interstitial-cta>${escapeHtml(cta)}</a>
602                <button type="button" class="ffs-interstitial__dismiss" data-interstitial-dismiss>Not now, continue to site</button>
603              </div>
604              <p class="ffs-interstitial__terms">${escapeHtml(terms)}</p>
605            </div>
606
607            <aside class="ffs-interstitial__benefits" aria-label="Chief Scout features">
608              <h2 class="ffs-interstitial__benefits-title">${escapeHtml(creative.benefitsTitle)}</h2>
609              <p class="ffs-interstitial__benefits-intro">${escapeHtml(creative.benefitsIntro)}</p>
610              <ul class="ffs-interstitial__features">
611                ${creative.features
612                  .map(function (feature) {
613                    return featureMarkup(feature[0], feature[1]);
614                  })
615                  .join("")}
616              </ul>
617            </aside>
618          </div>
619        </div>
620      </section>`;
621  }
622
623  function featureMarkup(title, description) {
624    return `
625      <li class="ffs-interstitial__feature">
626        <span class="ffs-interstitial__feature-icon" aria-hidden="true">${checkIcon()}</span>
627        <strong>${escapeHtml(title)}</strong>
628        <span class="ffs-interstitial__feature-copy">${escapeHtml(description)}</span>
629      </li>`;
630  }
631
632  function checkIcon() {
633    return '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m4.5 10.25 3.35 3.35L15.7 5.8" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
634  }
635
636  function closeIcon() {
637    return '<svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="m5 5 10 10M15 5 5 15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
638  }
639
640  function handleKeydown(event) {
641    if (event.key === "Escape") {
642      trackEvent("dismiss_escape", getAudience());
643      closeInterstitial();
644      return;
645    }
646
647    if (event.key !== "Tab") {
648      return;
649    }
650
651    const interstitial = document.getElementById(interstitialId);
652    if (!interstitial) {
653      return;
654    }
655
656    const focusable = interstitial.querySelectorAll(
657      'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',
658    );
659    if (!focusable.length) {
660      event.preventDefault();
661      return;
662    }
663
664    const first = focusable[0];
665    const last = focusable[focusable.length - 1];
666    if (event.shiftKey && document.activeElement === first) {
667      event.preventDefault();
668      last.focus();
669    } else if (!event.shiftKey && document.activeElement === last) {
670      event.preventDefault();
671      first.focus();
672    }
673  }
674
675  function closeInterstitial() {
676    const interstitial = document.getElementById(interstitialId);
677    document.removeEventListener("keydown", handleKeydown);
678    stopViewportTracking();
679    document.documentElement.classList.remove(
680      "ffs-scout-interstitial-pending",
681      "ffs-scout-interstitial-open",
682      "ffs-ucl-interstitial",
683    );
684
685    if (interstitial) {
686      interstitial.remove();
687    }
688  }
689
690  function failOpen() {
691    if (bodyObserver) {
692      bodyObserver.disconnect();
693      bodyObserver = null;
694    }
695    if (failOpenTimer) {
696      window.clearTimeout(failOpenTimer);
697      failOpenTimer = null;
698    }
699    document.documentElement.classList.remove(
700      "ffs-scout-interstitial-pending",
701      "ffs-scout-interstitial-open",
702      "ffs-ucl-interstitial",
703    );
704  }
705
706  // Keep the full headline within two lines at the largest fitting size.
707  function fitTitle() {
708    const title = document.getElementById("ffs-interstitial-title");
709    if (!title || !title.clientWidth) return;
710
711    title.style.removeProperty("font-size");
712    let upper = parseFloat(window.getComputedStyle(title).fontSize);
713    let lower = 1;
714    function fits() {
715      const lineHeight = parseFloat(window.getComputedStyle(title).lineHeight);
716      return title.getBoundingClientRect().height <= lineHeight * 2 + 0.5 &&
717        title.scrollWidth <= title.clientWidth + 1;
718    }
719    if (fits()) return;
720
721    while (upper - lower > 0.25) {
722      const size = (lower + upper) / 2;
723      title.style.fontSize = size + "px";
724      if (fits()) lower = size;
725      else upper = size;
726    }
727    title.style.fontSize = lower + "px";
728  }
729
730  function startTitleFitting() {
731    fitTitle();
732    if (document.fonts) document.fonts.ready.then(fitTitle);
733    if (window.ResizeObserver) {
734      let previousWidth = 0;
735      titleResizeObserver = new ResizeObserver(function (entries) {
736        const width = entries[0].contentRect.width;
737        if (width !== previousWidth) {
738          previousWidth = width;
739          fitTitle();
740        }
741      });
742      titleResizeObserver.observe(document.getElementById("ffs-interstitial-title"));
743    }
744    window.addEventListener("resize", fitTitle);
745  }
746
747  function startViewportTracking() {
748    syncViewport();
749
750    if (window.visualViewport) {
751      viewportResizeHandler = syncViewport;
752      viewportScrollHandler = syncViewport;
753      window.visualViewport.addEventListener("resize", viewportResizeHandler);
754      window.visualViewport.addEventListener("scroll", viewportScrollHandler);
755    } else {
756      viewportResizeHandler = syncViewport;
757      window.addEventListener("resize", viewportResizeHandler);
758    }
759  }
760
761  function stopViewportTracking() {
762    if (titleResizeObserver) {
763      titleResizeObserver.disconnect();
764      titleResizeObserver = null;
765    }
766    window.removeEventListener("resize", fitTitle);
767    if (window.visualViewport) {
768      if (viewportResizeHandler) {
769        window.visualViewport.removeEventListener(
770          "resize",
771          viewportResizeHandler,
772        );
773      }
774      if (viewportScrollHandler) {
775        window.visualViewport.removeEventListener(
776          "scroll",
777          viewportScrollHandler,
778        );
779      }
780    } else if (viewportResizeHandler) {
781      window.removeEventListener("resize", viewportResizeHandler);
782    }
783
784    viewportResizeHandler = null;
785    viewportScrollHandler = null;
786    document.documentElement.style.removeProperty("--ffs-interstitial-vh");
787    document.documentElement.style.removeProperty("--ffs-interstitial-vw");
788    document.documentElement.style.removeProperty("--ffs-interstitial-top");
789    document.documentElement.style.removeProperty("--ffs-interstitial-left");
790  }
791
792  function syncViewport() {
793    const viewport = window.visualViewport;
794    const height = viewport ? viewport.height : window.innerHeight;
795    const width = viewport ? viewport.width : window.innerWidth;
796    const top = viewport ? viewport.offsetTop : 0;
797    const left = viewport ? viewport.offsetLeft : 0;
798
799    document.documentElement.style.setProperty(
800      "--ffs-interstitial-vh",
801      Math.max(1, height) + "px",
802    );
803    document.documentElement.style.setProperty(
804      "--ffs-interstitial-vw",
805      Math.max(1, width) + "px",
806    );
807    document.documentElement.style.setProperty(
808      "--ffs-interstitial-top",
809      Math.max(0, top) + "px",
810    );
811    document.documentElement.style.setProperty(
812      "--ffs-interstitial-left",
813      Math.max(0, left) + "px",
814    );
815  }
816
817  function getOrCreateFlashClickId() {
818    let clickId = safeLocalGet(flashClickStorageKey);
819    if (!clickId) clickId = safeCookieGet(flashClickCookieName);
820    if (!/^[a-z0-9][a-z0-9_-]{7,79}$/i.test(clickId || "")) {
821      clickId =
822        window.crypto && typeof window.crypto.randomUUID === "function"
823          ? window.crypto.randomUUID().toLowerCase()
824          : (Date.now().toString(36) + Math.random().toString(36).slice(2, 14)).toLowerCase();
825    }
826    safeLocalSet(flashClickStorageKey, clickId);
827    const secure = window.location.protocol === "https:" ? "; Secure" : "";
828    document.cookie =
829      encodeURIComponent(flashClickCookieName) +
830      "=" +
831      encodeURIComponent(clickId) +
832      "; Max-Age=604800; Path=/; SameSite=Lax" +
833      secure;
834    return clickId;
835  }
836
837  function recordFlashSaleClick(clickId, audience) {
838    if (!config.eventUrl || !config.eventNonce || !clickId) return;
839    const payload = new FormData();
840    payload.append("action", "ffs_flash_sale_click");
841    payload.append("nonce", config.eventNonce);
842    payload.append("click_id", clickId);
843    payload.append("audience", audience);
844    if (navigator.sendBeacon) {
845      navigator.sendBeacon(config.eventUrl, payload);
846      return;
847    }
848    window.fetch(config.eventUrl, {
849      method: "POST",
850      body: payload,
851      credentials: "same-origin",
852      keepalive: true,
853    }).catch(function () {});
854  }
855
856  function trackEvent(action, audience) {
857    window.dataLayer = window.dataLayer || [];
858    window.dataLayer.push({
859      event: "scout_interstitial_" + action,
860      scout_interstitial_audience: audience,
861      scout_interstitial_campaign: config.campaign || "new-season-flash-sale-ffs5",
862    });
863  }
864
865  function escapeHtml(value) {
866    return String(value == null ? "" : value)
867      .replace(/&/g, "&amp;")
868      .replace(/</g, "&lt;")
869      .replace(/>/g, "&gt;")
870      .replace(/"/g, "&quot;")
871      .replace(/'/g, "&#039;");
872  }
873})();

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.