PageSourceSearch

https://www.etxluxurylimos.com/assets/js/cookieconsent-config.js?v=42.f98bc2c62c

js etxluxurylimos.com collected 2026-10-04 01:01:14 UTC 58,438 bytes, 1,912 lines download raw bytes

1// cookieconsent-config.js
2
3import * as CookieConsent from "/assets/js/cookieconsent.esm.js";
4
5/**
6 * Per-site URLs - customize before deploy (see README section 10).
7 */
8const SITE_CONFIG = {
9  privacyPolicyUrl: "/privacy-policy",
10  contactUrl: "https://www.etxluxurylimos.com/contact-us/",
11};
12
13const CAT_NECESSARY = "necessary";
14const CAT_ANALYTICS = "analytics";
15const CAT_ADVERTISEMENT = "advertisement";
16const CAT_FUNCTIONALITY = "functionality";
17const CAT_SECURITY = "security";
18
19const SERVICE_AD_STORAGE = "ad_storage";
20const SERVICE_AD_USER_DATA = "ad_user_data";
21const SERVICE_AD_PERSONALIZATION = "ad_personalization";
22const SERVICE_ANALYTICS_STORAGE = "analytics_storage";
23const SERVICE_FUNCTIONALITY_STORAGE = "functionality_storage";
24const SERVICE_PERSONALIZATION_STORAGE = "personalization_storage";
25const SERVICE_SECURITY_STORAGE = "security_storage";
26
27const im = iframemanager();
28
29/**
30 * Convert legacy embed iframes pasted in CMS content to IframeManager placeholders.
31 */
32/**
33 * Shared embed runtime helpers for IframeManager placeholder conversion.
34 * Included by the consent config module and the head embed guard.
35 */
36function getYoutubeVideoId(url) {
37  if (!url) {
38    return null;
39  }
40
41  try {
42    const parsed = new URL(url, window.location.origin);
43    const host = parsed.hostname.replace(/^www\./, "");
44
45    if (host === "youtu.be") {
46      return parsed.pathname.slice(1).split("/")[0] || null;
47    }
48
49    if (host.includes("youtube.com") || host.includes("youtube-nocookie.com")) {
50      if (parsed.pathname.startsWith("/embed/")) {
51        return parsed.pathname.split("/")[2] || null;
52      }
53
54      return parsed.searchParams.get("v");
55    }
56  } catch (error) {
57    return null;
58  }
59
60  return null;
61}
62
63function getVimeoVideoId(url) {
64  if (!url) {
65    return null;
66  }
67
68  try {
69    const parsed = new URL(url, window.location.origin);
70    const host = parsed.hostname.replace(/^www\./, "");
71
72    if (host === "player.vimeo.com" && parsed.pathname.startsWith("/video/")) {
73      return parsed.pathname.split("/")[2] || null;
74    }
75
76    if (host === "vimeo.com") {
77      return parsed.pathname.split("/").filter(Boolean)[0] || null;
78    }
79  } catch (error) {
80    return null;
81  }
82
83  return null;
84}
85
86function getGoogleMapsEmbedId(url) {
87  if (!url) {
88    return null;
89  }
90
91  try {
92    const parsed = new URL(url, window.location.origin);
93    const host = parsed.hostname.replace(/^www\./, "");
94    if (host === "google.com" && parsed.pathname.startsWith("/maps/")) {
95      const suffix = `${parsed.pathname.slice("/maps/".length)}${parsed.search}`.replace(/^\//, "");
96      return suffix || null;
97    }
98    if (host === "maps.google.com") {
99      const suffix = `${parsed.pathname.replace(/^\//, "")}${parsed.search}`;
100      return suffix || null;
101    }
102  } catch (error) {
103    return null;
104  }
105
106  return null;
107}
108
109function getSpotifyEmbedId(url) {
110  if (!url) {
111    return null;
112  }
113
114  try {
115    const parsed = new URL(url, window.location.origin);
116    const host = parsed.hostname.replace(/^www\./, "");
117
118    if (!host.includes("spotify.com") || !parsed.pathname.includes("/embed/")) {
119      return null;
120    }
121
122    const suffix = `${parsed.pathname.split("/embed/")[1] || ""}${parsed.search}`.replace(/^\//, "");
123    return suffix || null;
124  } catch (error) {
125    return null;
126  }
127}
128
129function getSoundCloudEmbedId(url) {
130  if (!url) {
131    return null;
132  }
133
134  try {
135    const parsed = new URL(url, window.location.origin);
136    const host = parsed.hostname.replace(/^www\./, "");
137
138    if (!host.includes("soundcloud.com") || !parsed.pathname.includes("/player")) {
139      return null;
140    }
141
142    return parsed.search ? parsed.search.slice(1) : null;
143  } catch (error) {
144    return null;
145  }
146}
147
148function getInvestingWidgetEmbedId(url) {
149  if (!url) {
150    return null;
151  }
152
153  try {
154    const parsed = new URL(url, window.location.origin);
155    const host = parsed.hostname.replace(/^www\./, "");
156
157    if (!/(?:^|\.)forexprostools\.com$|(?:^|\.)investing\.com$/i.test(host)) {
158      return null;
159    }
160
161    return parsed.href;
162  } catch (error) {
163    return null;
164  }
165}
166
167function getFacebookEmbedId(url) {
168  if (!url) {
169    return null;
170  }
171
172  try {
173    const parsed = new URL(url, window.location.origin);
174    const host = parsed.hostname.replace(/^www\./, "");
175
176    if (!host.includes("facebook.com") || !parsed.pathname.includes("/plugins/")) {
177      return null;
178    }
179
180    const suffix = `${parsed.pathname.split("/plugins/")[1] || ""}${parsed.search}`.replace(/^\//, "");
181    return suffix || null;
182  } catch (error) {
183    return null;
184  }
185}
186
187function getCalendlyEmbedId(url) {
188  if (!url) {
189    return null;
190  }
191
192  try {
193    const parsed = new URL(url, window.location.origin);
194    const host = parsed.hostname.replace(/^www\./, "");
195
196    if (!host.includes("calendly.com")) {
197      return null;
198    }
199
200    const suffix = `${parsed.pathname.replace(/^\//, "")}${parsed.search}`.replace(/\/$/, "");
201    return suffix || null;
202  } catch (error) {
203    return null;
204  }
205}
206
207function normalizeGoogleMapsPlaceholders() {
208  document.querySelectorAll('[data-service="googlemaps"]').forEach((el) => {
209    const id = el.getAttribute("data-id");
210    if (id && !id.includes("?") && !id.includes("/")) {
211      el.setAttribute("data-id", `embed?pb=${id}`);
212    }
213  });
214}
215
216const KNOWN_EMBED_RATIOS = new Set([
217  "1:1",
218  "2:1",
219  "3:2",
220  "5:2",
221  "4:3",
222  "16:9",
223  "16:10",
224  "20:9",
225  "21:9",
226  "9:16",
227  "9:20",
228]);
229
230function gcd(a, b) {
231  return b ? gcd(b, a % b) : a;
232}
233
234function normalizeCssSize(value) {
235  if (!value) {
236    return value;
237  }
238  return /^\d+$/.test(value) ? `${value}px` : value;
239}
240
241function parsePixelSize(value) {
242  if (!value) {
243    return null;
244  }
245  if (/^\d+$/.test(value)) {
246    return parseInt(value, 10);
247  }
248  const match = /^(\d+(?:\.\d+)?)px$/i.exec(value.trim());
249  return match ? Math.round(parseFloat(match[1])) : null;
250}
251
252function isPercentageSize(value) {
253  return Boolean(value && String(value).trim().endsWith("%"));
254}
255
256function ratioForPixels(widthPx, heightPx) {
257  const divisor = gcd(widthPx, heightPx);
258  const key = `${widthPx / divisor}:${heightPx / divisor}`;
259  return KNOWN_EMBED_RATIOS.has(key) ? key : null;
260}
261
262function extractIframeDimensions(iframe) {
263  let width = iframe.getAttribute("width");
264  let height = iframe.getAttribute("height");
265  const style = iframe.getAttribute("style") || "";
266
267  style.split(";").forEach((chunk) => {
268    if (!chunk.includes(":")) {
269      return;
270    }
271    const [rawKey, rawValue] = chunk.split(":");
272    const key = rawKey.trim().toLowerCase();
273    const value = rawValue.trim();
274    if (key === "width" && !width) {
275      width = value;
276    }
277    if (key === "height" && !height) {
278      height = value;
279    }
280  });
281
282  return { width, height };
283}
284
285function applyDimensionsToPlaceholder(placeholder, { width, height }) {
286  if (!width && !height) {
287    placeholder.setAttribute("data-autoscale", "");
288    return;
289  }
290
291  const styleParts = [];
292  if (width) {
293    styleParts.push(`width:${normalizeCssSize(width)}`);
294  }
295  if (height) {
296    styleParts.push(`height:${normalizeCssSize(height)}`);
297  }
298  styleParts.push("margin:0 auto");
299  placeholder.setAttribute("style", styleParts.join(";"));
300
301  const widthPx = parsePixelSize(width);
302  const heightPx = parsePixelSize(height);
303  if (widthPx && heightPx) {
304    const ratio = ratioForPixels(widthPx, heightPx);
305    if (ratio) {
306      placeholder.setAttribute("data-ratio", ratio);
307      return;
308    }
309    placeholder.classList.add("im-fixed-size");
310    return;
311  }
312
313  if (isPercentageSize(height)) {
314    placeholder.setAttribute("style", "position:absolute;top:0;left:0;width:100%;height:100%");
315    placeholder.setAttribute("data-autoscale", "");
316    return;
317  }
318
319  if (height || (width && !String(width).includes("%"))) {
320    placeholder.classList.add("im-fixed-size");
321    return;
322  }
323
324  placeholder.setAttribute("data-autoscale", "");
325}
326
327function getEmbedQueryParams(url) {
328  if (!url) {
329    return "";
330  }
331
332  try {
333    return new URL(url, window.location.origin).searchParams.toString();
334  } catch (error) {
335    return "";
336  }
337}
338
339function getLegacyEmbedTarget(iframe) {
340  const src = iframe.getAttribute("src")
341    || iframe.getAttribute("data-src")
342    || iframe.getAttribute("data-cc-blocked-embed-src")
343    || "";
344
345  if (!src) {
346    return null;
347  }
348
349  const youtubeId = getYoutubeVideoId(src);
350
351  if (youtubeId) {
352    return {
353      service: "youtube",
354      id: youtubeId,
355      params: getEmbedQueryParams(src),
356    };
357  }
358
359  const vimeoVideoId = getVimeoVideoId(src);
360
361  if (vimeoVideoId) {
362    return {
363      service: "vimeo",
364      id: vimeoVideoId,
365      params: getEmbedQueryParams(src),
366    };
367  }
368
369  const mapsId = src.includes("google.com/maps") || src.includes("maps.google")
370    ? getGoogleMapsEmbedId(src)
371    : null;
372
373  if (mapsId) {
374    return {
375      service: "googlemaps",
376      id: mapsId,
377      params: "",
378    };
379  }
380
381  if (/spotify\.com\/embed/i.test(src)) {
382    const spotifyId = getSpotifyEmbedId(src);
383    if (spotifyId) {
384      return {
385        service: "spotify",
386        id: spotifyId,
387        params: "",
388      };
389    }
390  }
391
392  if (/soundcloud\.com\/player/i.test(src)) {
393    const soundcloudId = getSoundCloudEmbedId(src);
394    if (soundcloudId) {
395      return {
396        service: "soundcloud",
397        id: soundcloudId,
398        params: "",
399      };
400    }
401  }
402
403  if (/facebook\.com\/plugins/i.test(src)) {
404    const facebookId = getFacebookEmbedId(src);
405    if (facebookId) {
406      return {
407        service: "facebook_social",
408        id: facebookId,
409        params: "",
410      };
411    }
412  }
413
414  if (/forexprostools\.com|investing\.com/i.test(src)) {
415    const investingId = getInvestingWidgetEmbedId(src);
416    if (investingId) {
417      return {
418        service: "investing_widget",
419        id: investingId,
420        params: "",
421      };
422    }
423  }
424
425  if (/calendly\.com/i.test(src)) {
426    const calendlyId = getCalendlyEmbedId(src);
427    if (calendlyId) {
428      return {
429        service: "calendly",
430        id: calendlyId,
431        params: "",
432      };
433    }
434  }
435
436  return null;
437}
438
439function copyIframeAttributesToPlaceholder(iframe, placeholder) {
440  const passthroughAttributes = ["title", "loading", "referrerpolicy"];
441
442  passthroughAttributes.forEach((attribute) => {
443    const value = iframe.getAttribute(attribute);
444
445    if (!value) {
446      return;
447    }
448
449    if (attribute === "title") {
450      placeholder.setAttribute("data-title", value);
451      return;
452    }
453
454    placeholder.setAttribute(`data-iframe-${attribute}`, value);
455  });
456
457  iframe.getAttributeNames().forEach((attribute) => {
458    if (!attribute.startsWith("data-iframe-")) {
459      return;
460    }
461
462    placeholder.setAttribute(attribute, iframe.getAttribute(attribute));
463  });
464}
465function migrateLegacyCmsEmbeds() {
466  document
467    .querySelectorAll("iframe[src], iframe[data-src], iframe[data-cc-blocked-embed-src]")
468    .forEach((iframe) => {
469      if (iframe.closest("[data-service]")) {
470        return;
471      }
472
473      const target = getLegacyEmbedTarget(iframe);
474
475      if (!target) {
476        return;
477      }
478
479      const placeholder = document.createElement("div");
480      placeholder.setAttribute("data-service", target.service);
481      placeholder.setAttribute("data-id", target.id);
482
483      applyDimensionsToPlaceholder(placeholder, extractIframeDimensions(iframe));
484
485      if (target.params) {
486        placeholder.setAttribute("data-params", target.params);
487      }
488
489      copyIframeAttributesToPlaceholder(iframe, placeholder);
490      iframe.replaceWith(placeholder);
491    });
492}
493
494migrateLegacyCmsEmbeds();
495normalizeGoogleMapsPlaceholders();
496
497/**
498 * Update gtag consent according to the users choices made in CookieConsent UI
499 */
500function updateGtagConsent() {
501  if (typeof gtag !== "function") return;
502
503  gtag("consent", "update", {
504    [SERVICE_ANALYTICS_STORAGE]: CookieConsent.acceptedService(
505      SERVICE_ANALYTICS_STORAGE,
506      CAT_ANALYTICS,
507    )
508      ? "granted"
509      : "denied",
510    [SERVICE_AD_STORAGE]: CookieConsent.acceptedService(
511      SERVICE_AD_STORAGE,
512      CAT_ADVERTISEMENT,
513    )
514      ? "granted"
515      : "denied",
516    [SERVICE_AD_USER_DATA]: CookieConsent.acceptedService(
517      SERVICE_AD_USER_DATA,
518      CAT_ADVERTISEMENT,
519    )
520      ? "granted"
521      : "denied",
522    [SERVICE_AD_PERSONALIZATION]: CookieConsent.acceptedService(
523      SERVICE_AD_PERSONALIZATION,
524      CAT_ADVERTISEMENT,
525    )
526      ? "granted"
527      : "denied",
528    [SERVICE_FUNCTIONALITY_STORAGE]: CookieConsent.acceptedService(
529      SERVICE_FUNCTIONALITY_STORAGE,
530      CAT_FUNCTIONALITY,
531    )
532      ? "granted"
533      : "denied",
534    [SERVICE_PERSONALIZATION_STORAGE]: CookieConsent.acceptedService(
535      SERVICE_PERSONALIZATION_STORAGE,
536      CAT_FUNCTIONALITY,
537    )
538      ? "granted"
539      : "denied",
540    [SERVICE_SECURITY_STORAGE]: CookieConsent.acceptedService(
541      SERVICE_SECURITY_STORAGE,
542      CAT_SECURITY,
543    )
544      ? "granted"
545      : "denied",
546  });
547}
548
549function updateWidgetPlaceholders() {
550  document.querySelectorAll("[data-cc-placeholder]").forEach((el) => {
551    const category = el.getAttribute("data-cc-placeholder");
552    const service = el.getAttribute("data-cc-service");
553
554    if (service === "elfsight" || service === "yext_analytics" || service === "weatherwidget" || service === "tradingview_widget" || service === "iprospector" || service === "paragon_mls" || service === "googlemaps_js" || service === "inksoft" || service === "appfolio") {
555      return;
556    }
557
558    let accepted = false;
559    if (service) {
560      accepted = CookieConsent.acceptedService(service, category);
561    } else if (category) {
562      accepted = CookieConsent.acceptedCategory(category);
563    }
564
565    el.toggleAttribute("hidden", accepted);
566  });
567}
568function isImGooglemapsGranted() {
569  if (!widgetConsentGranted("googlemaps", CAT_FUNCTIONALITY)) return false;
570  if (!/\/mls(?:\/|$)/.test(location.pathname || "")) return true;
571  if (CookieConsent.acceptedCategory(CAT_FUNCTIONALITY)) return true;
572  return CookieConsent.acceptedService("paragon_mls", CAT_FUNCTIONALITY);
573}
574
575function syncIframeManagerConsent() {
576  const sync = (service, category) => {
577    if (widgetConsentGranted(service, category)) {
578      im.acceptService(service);
579    } else {
580      im.rejectService(service);
581    }
582  };
583  sync("youtube", CAT_FUNCTIONALITY);
584  sync("vimeo", CAT_FUNCTIONALITY);
585  if (isImGooglemapsGranted()) {
586    im.acceptService("googlemaps");
587  } else {
588    im.rejectService("googlemaps");
589  }
590  sync("spotify", CAT_FUNCTIONALITY);
591  sync("soundcloud", CAT_FUNCTIONALITY);
592  sync("facebook_social", CAT_FUNCTIONALITY);
593  sync("calendly", CAT_FUNCTIONALITY);
594}
595
596// widget-consent-common:v1
597function widgetConsentGranted(serviceId, category) {
598  return CookieConsent.acceptedService(serviceId, category)
599    || CookieConsent.acceptedCategory(category);
600}
601
602
603// recaptcha-consent:v22
604const RECAPTCHA_CONSENT_VERSION = "v18";
605let recaptchaScriptPromise = null;
606let recaptchaScriptSrcLoaded = null;
607let recaptchaPreferExplicit = false;
608let recaptchaConfigError = false;
609
610function markRecaptchaConfigError() {
611  recaptchaConfigError = true;
612}
613
614function whenRecaptchaReady() {
615  if (!window.grecaptcha?.ready) {
616    return Promise.resolve();
617  }
618  return new Promise((resolve) => grecaptcha.ready(resolve));
619}
620
621function isValidRecaptchaSitekey(key) {
622  return typeof key === "string" && /^6L[\w-]{30,}$/.test(key);
623}
624
625function getRecaptchaForm(preferred) {
626  if (preferred && preferred.tagName === "FORM") {
627    return preferred;
628  }
629  return document.getElementById("theForm")
630    || document.getElementById("ContactForm")
631    || document.querySelector("form.grid-form")
632    || document.querySelector("form");
633}
634
635function extractRecaptchaSitekeyFromScripts(root) {
636  const scope = root && root.querySelectorAll ? root : document;
637  for (const script of scope.querySelectorAll(
638    'script[src*="recaptcha/api.js"], script[src*="recaptcha/enterprise.js"], script[data-cc-blocked-recaptcha-src*="recaptcha/"]'
639  )) {
640    const src = script.getAttribute("src")
641      || script.getAttribute("data-cc-blocked-recaptcha-src")
642      || "";
643    const match = src.match(/[?&]render=([^&]+)/);
644    const candidate = match?.[1] ? decodeURIComponent(match[1]) : null;
645    if (isValidRecaptchaSitekey(candidate)) {
646      return candidate;
647    }
648  }
649  return null;
650}
651
652function extractRecaptchaSitekeyFromInline() {
653  for (const script of document.scripts) {
654    const body = script.textContent || "";
655    const match = body.match(/grecaptcha\.execute\s*\(\s*['"](6L[^'"]+)['"]/i);
656    if (match?.[1] && isValidRecaptchaSitekey(match[1])) {
657      return match[1];
658    }
659  }
660  return null;
661}
662
663function metadataSitekeyFromScope(root) {
664  if (!root || !root.querySelector) {
665    return null;
666  }
667  const candidate = root.querySelector(
668    ".cookie-captcha-metadata[data-recaptcha-sitekey]"
669  )?.getAttribute("data-recaptcha-sitekey")
670    || root.querySelector("[data-recaptcha-sitekey]")?.getAttribute("data-recaptcha-sitekey")
671    || root.querySelector(".g-recaptcha")?.getAttribute("data-sitekey")
672    || null;
673  return isValidRecaptchaSitekey(candidate) ? candidate : null;
674}
675
676function getRecaptchaSitekey(form) {
677  const targetForm = getRecaptchaForm(form);
678  const fromFormBlocked = targetForm && extractRecaptchaSitekeyFromScripts(targetForm);
679  const fromBlocked = fromFormBlocked || extractRecaptchaSitekeyFromScripts(document);
680  const fromInline = extractRecaptchaSitekeyFromInline();
681  const fromFormMetadata = metadataSitekeyFromScope(targetForm);
682  const fromAncestor = targetForm?.closest("[data-recaptcha-sitekey]")
683    ?.getAttribute("data-recaptcha-sitekey") || null;
684  const fromValidAncestor = isValidRecaptchaSitekey(fromAncestor) ? fromAncestor : null;
685  const fromGlobal = metadataSitekeyFromScope(document);
686  return fromBlocked || fromInline || fromFormMetadata || fromValidAncestor || fromGlobal || null;
687}
688
689function getRecaptchaVersion(form) {
690  if (recaptchaPreferExplicit) {
691    return "v2";
692  }
693  const targetForm = getRecaptchaForm(form);
694  // GroupM7 contact forms use buttonClick + grecaptcha.execute (v3), not checkbox widgets.
695  if (formUsesButtonClickSubmit(targetForm)) {
696    return "v3";
697  }
698  if (targetForm?.querySelector(".g-recaptcha") || document.querySelector(".g-recaptcha")) {
699    return "v2";
700  }
701  const explicit = targetForm?.querySelector("[data-recaptcha-version]")?.getAttribute("data-recaptcha-version")
702    || document.querySelector("[data-recaptcha-version]")?.getAttribute("data-recaptcha-version");
703  if (explicit) {
704    return explicit;
705  }
706  const scope = targetForm || document;
707  if (scope.querySelector("#g-recaptcha-response") || scope.querySelector("[name='g-recaptcha-response']")) {
708    return "v3";
709  }
710  return "v2";
711}
712
713function recaptchaScriptSrc(options = {}) {
714  const forceExplicit = Boolean(options.forceExplicit) || recaptchaPreferExplicit;
715  if (forceExplicit) {
716    return "https://www.google.com/recaptcha/api.js?render=explicit";
717  }
718  const sitekey = getRecaptchaSitekey(options.form);
719  const version = getRecaptchaVersion(options.form);
720  if (version === "v3" && sitekey) {
721    return `https://www.google.com/recaptcha/api.js?render=${encodeURIComponent(sitekey)}`;
722  }
723  return "https://www.google.com/recaptcha/api.js?render=explicit";
724}
725
726function getRecaptchaAction(form) {
727  const targetForm = getRecaptchaForm(form);
728  return targetForm?.querySelector("[data-recaptcha-action]")?.getAttribute("data-recaptcha-action")
729    || document.querySelector("[data-recaptcha-action]")?.getAttribute("data-recaptcha-action")
730    || "contact";
731}
732
733function purgeStaleRecaptchaScripts() {
734  document.querySelectorAll("script[src*='google.com/recaptcha/api.js']").forEach((script) => {
735    script.remove();
736  });
737  if (window.grecaptcha && recaptchaScriptSrcLoaded !== recaptchaScriptSrc()) {
738    delete window.grecaptcha;
739    recaptchaScriptPromise = null;
740    recaptchaScriptSrcLoaded = null;
741  }
742}
743
744function loadRecaptchaScript(options = {}) {
745  const bypassConsent = Boolean(options.bypassConsent);
746  const forceExplicit = Boolean(options.forceExplicit);
747  const form = getRecaptchaForm(options.form);
748  if (!bypassConsent && !isRecaptchaAccepted()) {
749    return Promise.reject(new Error("reCAPTCHA consent required"));
750  }
751  const src = recaptchaScriptSrc({ forceExplicit, form });
752  if (getRecaptchaVersion(form) === "v3" && !getRecaptchaSitekey(form)) {
753    return Promise.reject(new Error("reCAPTCHA site key missing"));
754  }
755  purgeStaleRecaptchaScripts();
756  if (window.grecaptcha?.ready && recaptchaScriptSrcLoaded === src) {
757    return whenRecaptchaReady();
758  }
759  if (recaptchaScriptPromise && recaptchaScriptSrcLoaded === src) {
760    return recaptchaScriptPromise;
761  }
762  recaptchaScriptPromise = new Promise((resolve, reject) => {
763    const script = document.createElement("script");
764    script.setAttribute("data-cookie-recaptcha-loader", "1");
765    script.async = true;
766    script.defer = true;
767    script.src = src;
768    script.onload = () => {
769      recaptchaScriptSrcLoaded = src;
770      whenRecaptchaReady().then(resolve, reject);
771    };
772    script.onerror = () => {
773      if (!forceExplicit && /[?&]render=/.test(src) && !src.includes("render=explicit")) {
774        recaptchaScriptPromise = null;
775        recaptchaScriptSrcLoaded = null;
776        markRecaptchaConfigError();
777        script.remove();
778        reject(new Error("reCAPTCHA configuration error"));
779        return;
780      }
781      reject(new Error("reCAPTCHA failed to load"));
782    };
783    document.head.appendChild(script);
784  });
785  return recaptchaScriptPromise;
786}
787
788function renderRecaptchaWidgets() {
789  return whenRecaptchaReady().then(() => {
790    if (!window.grecaptcha?.render) {
791      return;
792    }
793    document.querySelectorAll(".g-recaptcha:not([data-recaptcha-ready])").forEach((el) => {
794      const sitekey = el.getAttribute("data-sitekey");
795      if (!sitekey) {
796        return;
797      }
798      try {
799        grecaptcha.render(el, { sitekey });
800        el.setAttribute("data-recaptcha-ready", "1");
801      } catch (error) {
802        /* widget may already be rendered */
803      }
804    });
805  });
806}
807
808async function executeRecaptchaV3(form) {
809  const targetForm = getRecaptchaForm(form);
810  const sitekey = getRecaptchaSitekey(targetForm);
811  if (!sitekey || !window.grecaptcha?.execute) {
812    return false;
813  }
814  await whenRecaptchaReady();
815  const action = getRecaptchaAction(targetForm);
816  try {
817    const token = await grecaptcha.execute(sitekey, { action });
818    setRecaptchaResponseToken(token, targetForm);
819    return Boolean(token);
820  } catch (error) {
821    markRecaptchaConfigError();
822    return false;
823  }
824}
825
826function setRecaptchaResponseToken(token, form) {
827  const targetForm = getRecaptchaForm(form);
828  let responseEl = (targetForm && targetForm.querySelector("[name='g-recaptcha-response']"))
829    || document.getElementById("g-recaptcha-response")
830    || document.querySelector("[name='g-recaptcha-response']");
831  if (!responseEl && targetForm) {
832    responseEl = document.createElement("input");
833    responseEl.type = "hidden";
834    responseEl.name = "g-recaptcha-response";
835    responseEl.id = "g-recaptcha-response";
836    targetForm.appendChild(responseEl);
837  }
838  if (responseEl) {
839    responseEl.value = token;
840  }
841  const dupEl = targetForm && targetForm.querySelector("[name='g_recaptcha_response']");
842  if (dupEl) {
843    dupEl.value = token;
844  }
845  return responseEl;
846}
847
848function ensureV2RecaptchaWidget(form) {
849  const targetForm = getRecaptchaForm(form);
850  if (!targetForm || targetForm.querySelector(".g-recaptcha")) {
851    return false;
852  }
853  const sitekey = getRecaptchaSitekey(targetForm);
854  if (!sitekey) {
855    return false;
856  }
857  const div = document.createElement("div");
858  div.className = "g-recaptcha";
859  div.setAttribute("data-sitekey", sitekey);
860  const submit = targetForm.querySelector(
861    'button:not([type]), button[type="submit"], input[type="submit"]'
862  );
863  if (submit) {
864    submit.insertAdjacentElement("beforebegin", div);
865  } else {
866    targetForm.appendChild(div);
867  }
868  document.querySelectorAll("[data-recaptcha-version]").forEach((el) => {
869    el.setAttribute("data-recaptcha-version", "v2");
870  });
871  return true;
872}
873
874function v2WidgetAwaitingCheckbox(form) {
875  const targetForm = form || document.getElementById("ContactForm") || document.querySelector("form");
876  if (!targetForm) {
877    return false;
878  }
879  const widget = targetForm.querySelector(".g-recaptcha");
880  return Boolean(widget && widget.getAttribute("data-recaptcha-ready") === "1");
881}
882
883async function activateRecaptcha(options = {}) {
884  const bypassConsent = Boolean(options.bypassConsent);
885  if (!bypassConsent && !isRecaptchaAccepted()) {
886    return false;
887  }
888  const form = getRecaptchaForm(options.form);
889  try {
890    await loadRecaptchaScript({ bypassConsent, form });
891  } catch (error) {
892    return false;
893  }
894  if (getRecaptchaVersion(form) === "v3") {
895    document.querySelectorAll(".g-recaptcha").forEach((el) => {
896      el.hidden = true;
897    });
898    const executed = await executeRecaptchaV3(form);
899    if (executed) {
900      return true;
901    }
902    if (recaptchaConfigError) {
903      return false;
904    }
905    ensureV2RecaptchaWidget(form);
906    await renderRecaptchaWidgets();
907    return true;
908  }
909  ensureV2RecaptchaWidget(form);
910  await renderRecaptchaWidgets();
911  return true;
912}
913
914function isRecaptchaAccepted() {
915  return CookieConsent.acceptedService("google_recaptcha", CAT_SECURITY);
916}
917
918function acceptRecaptchaConsent() {
919  const prefs = CookieConsent.getUserPreferences();
920  const categories = [...new Set([
921    CAT_NECESSARY,
922    ...(prefs.acceptedCategories || []),
923    CAT_SECURITY,
924  ])];
925  CookieConsent.acceptCategory(categories);
926  CookieConsent.acceptService("google_recaptcha", CAT_SECURITY);
927}
928
929function updateCaptchaConsent() {
930  const accepted = isRecaptchaAccepted();
931  document.querySelectorAll(".g-recaptcha").forEach((el) => {
932    if (accepted && el.hasAttribute("data-recaptcha-ready")) {
933      el.setAttribute("data-recaptcha-ready", "1");
934    } else if (!accepted) {
935      el.removeAttribute("data-recaptcha-ready");
936    }
937  });
938}
939
940async function enableRecaptcha(event) {
941  if (event?.preventDefault) {
942    event.preventDefault();
943    event.stopPropagation();
944  }
945  acceptRecaptchaConsent();
946  try {
947    const ok = await activateRecaptcha({ bypassConsent: true });
948    updateCaptchaConsent();
949    return ok;
950  } catch (error) {
951    updateCaptchaConsent();
952    return false;
953  }
954}
955
956async function preloadRecaptchaOnConsent() {
957  if (!pageHasRecaptchaMarkup()) {
958    return false;
959  }
960  try {
961    const ok = await activateRecaptcha({ bypassConsent: true });
962    updateCaptchaConsent();
963    return ok;
964  } catch (error) {
965    return false;
966  }
967}
968
969window.enableRecaptcha = enableRecaptcha;
970window.activateRecaptcha = activateRecaptcha;
971window.isRecaptchaAccepted = isRecaptchaAccepted;
972window.preloadRecaptchaOnConsent = preloadRecaptchaOnConsent;
973
974function ensureCaptchaMetadata() {
975  if (document.querySelector("[data-cc-captcha]")) {
976    return false;
977  }
978  const form = getRecaptchaForm();
979  const sitekey = getRecaptchaSitekey(form);
980  const version =
981    form?.querySelector("[data-recaptcha-version]")?.getAttribute("data-recaptcha-version")
982    || document.querySelector("[data-recaptcha-version]")?.getAttribute("data-recaptcha-version")
983    || (form?.querySelector(".g-recaptcha") || document.querySelector(".g-recaptcha") ? "v2" : "v3");
984  if (!form || !sitekey) {
985    return false;
986  }
987  const block = document.createElement("div");
988  block.className = "cookie-captcha-metadata";
989  block.setAttribute("data-cc-captcha", "security");
990  block.setAttribute("data-cc-service", "google_recaptcha");
991  block.setAttribute("data-recaptcha-sitekey", sitekey);
992  block.setAttribute("data-recaptcha-version", version);
993  block.setAttribute("hidden", "");
994  block.setAttribute("aria-hidden", "true");
995  const anchor = document.getElementById("g-recaptcha-response")
996    || document.querySelector("[name='g-recaptcha-response']");
997  if (anchor?.parentNode) {
998    anchor.parentNode.insertBefore(block, anchor);
999  } else {
1000    form.appendChild(block);
1001  }
1002  return true;
1003}
1004
1005function findCaptchaProtectedForms() {
1006  const forms = [];
1007  const contact = document.getElementById("ContactForm");
1008  if (contact && formHasRecaptcha(contact)) {
1009    forms.push(contact);
1010  }
1011  document.querySelectorAll("form").forEach((form) => {
1012    if (form === contact || forms.includes(form)) {
1013      return;
1014    }
1015    if (formHasRecaptcha(form)) {
1016      forms.push(form);
1017    }
1018  });
1019  return forms;
1020}
1021
1022function findExternalButtonClickTrigger(form) {
1023  if (!form || !formHasRecaptcha(form)) {
1024    return null;
1025  }
1026  const external = document.getElementById("submitButton");
1027  if (external && /buttonClick/.test(external.getAttribute("onclick") || "")) {
1028    return external;
1029  }
1030  return null;
1031}
1032
1033function findSubmitNoticeAnchor(form) {
1034  if (!form) {
1035    return null;
1036  }
1037  const inForm = form.querySelector("#submitButton")
1038    || form.querySelector('[onclick*="buttonClick"]')
1039    || form.querySelector(
1040      'button:not([type]), button[type="submit"], input[type="submit"]'
1041    );
1042  if (inForm) {
1043    return inForm;
1044  }
1045  return findExternalButtonClickTrigger(form);
1046}
1047
1048function submitAnchorHasNotice(submit) {
1049  return Boolean(
1050    submit?.nextElementSibling?.classList?.contains("cookie-captcha-submit-notice")
1051  );
1052}
1053
1054function formHasSubmitNotice(form) {
1055  if (form.querySelector(".cookie-captcha-submit-notice")) {
1056    return true;
1057  }
1058  return submitAnchorHasNotice(findSubmitNoticeAnchor(form));
1059}
1060
1061function ensureCaptchaSubmitNotice() {
1062  const captchaForms = findCaptchaProtectedForms();
1063  if (!captchaForms.length) {
1064    return false;
1065  }
1066  let added = false;
1067  captchaForms.forEach((form) => {
1068    if (formHasSubmitNotice(form)) {
1069      return;
1070    }
1071    const submit = findSubmitNoticeAnchor(form);
1072    if (!submit) {
1073      return;
1074    }
1075    const notice = document.createElement("p");
1076    notice.className = "cookie-captcha-submit-notice";
1077    notice.textContent = "Submitting this form gives consent to security verification (reCAPTCHA).";
1078    submit.insertAdjacentElement("afterend", notice);
1079    added = true;
1080  });
1081  return added;
1082}
1083
1084function isRecaptchaPostField(name) {
1085  return /recaptcha/i.test(name || "");
1086}
1087
1088function captchaActivationFailedMessage() {
1089  if (recaptchaConfigError) {
1090    return "Security verification is not available on this site. Please contact the site owner.";
1091  }
1092  if (!getRecaptchaSitekey()) {
1093    return "Security verification is not configured on this form. Enable Security cookies in Cookie Settings, then try again.";
1094  }
1095  return "Security verification failed to load. Enable Security cookies in Cookie Settings, then try again.";
1096}
1097
1098function captchaIncompleteMessage() {
1099  if (getRecaptchaVersion() === "v2") {
1100    return "Please complete the security checkbox, then submit again.";
1101  }
1102  return captchaActivationFailedMessage();
1103}
1104
1105function formUsesContactVerifyFlow(form) {
1106  const targetForm = getRecaptchaForm(form);
1107  return Boolean(targetForm && targetForm.id === "ContactForm");
1108}
1109
1110function setHiddenFormField(form, name, value) {
1111  if (!form) {
1112    return null;
1113  }
1114  let field = form.querySelector(`[name="${name}"]`);
1115  if (!field) {
1116    field = document.createElement("input");
1117    field.type = "hidden";
1118    field.name = name;
1119    form.appendChild(field);
1120  }
1121  field.value = value;
1122  return field;
1123}
1124
1125async function verifyRecaptchaBeforeContactSubmit(token, submitUrl, form) {
1126  const verifyBody = new URLSearchParams();
1127  verifyBody.set("cookie_recaptcha_verify", "1");
1128  verifyBody.set("g-recaptcha-response", token);
1129  verifyBody.set("cookie_recaptcha_version", getRecaptchaVersion(form));
1130  const response = await fetch(submitUrl, {
1131    method: "POST",
1132    headers: { "Content-Type": "application/x-www-form-urlencoded" },
1133    body: verifyBody.toString(),
1134    credentials: "same-origin",
1135    redirect: "manual",
1136  });
1137  return response.status === 204
1138    || response.headers.get("X-Cookie-Recaptcha-Verified") === "1";
1139}
1140
1141async function submitContactFormWithRecaptcha(preferredForm) {
1142  const form = getRecaptchaForm(preferredForm);
1143  let tokenEl = (form && form.querySelector("[name='g-recaptcha-response']"))
1144    || document.getElementById("g-recaptcha-response")
1145    || document.querySelector("[name='g-recaptcha-response']");
1146  const tokenNeedsRefresh = !tokenEl || !tokenEl.value || tokenEl.value.length < 100;
1147  if (tokenNeedsRefresh) {
1148    let activated = false;
1149    if (isRecaptchaAccepted()) {
1150      try {
1151        activated = await activateRecaptcha({ form });
1152      } catch (error) {
1153        activated = false;
1154      }
1155    } else {
1156      activated = await enableRecaptcha();
1157    }
1158    if (!activated) {
1159      alert(captchaActivationFailedMessage());
1160      return;
1161    }
1162    tokenEl = (form && form.querySelector("[name='g-recaptcha-response']"))
1163      || document.getElementById("g-recaptcha-response")
1164      || document.querySelector("[name='g-recaptcha-response']");
1165    if (getRecaptchaVersion(form) === "v2" && (!tokenEl || !tokenEl.value)) {
1166      if (v2WidgetAwaitingCheckbox(form)) {
1167        const widget = form?.querySelector(".g-recaptcha");
1168        if (widget) {
1169          widget.scrollIntoView({ behavior: "smooth", block: "nearest" });
1170        }
1171        return;
1172      }
1173    }
1174  }
1175  let submitUrl = form ? (form.action || window.location.href) : window.location.href;
1176  if (form) {
1177    try {
1178      const parsed = new URL(submitUrl, window.location.href);
1179      if (!parsed.searchParams.has("submitted")) {
1180        parsed.searchParams.set("submitted", "");
1181      }
1182      submitUrl = parsed.href;
1183      form.action = submitUrl;
1184    } catch (error) {
1185      if (!submitUrl.includes("submitted")) {
1186        submitUrl += (submitUrl.includes("?") ? "&" : "?") + "submitted";
1187        form.action = submitUrl;
1188      }
1189    }
1190  }
1191  if (!tokenEl || !tokenEl.value) {
1192    alert(captchaIncompleteMessage());
1193    return;
1194  }
1195  if (!form) {
1196    return;
1197  }
1198  if (!formUsesContactVerifyFlow(form)) {
1199    setHiddenFormField(form, "cookie_recaptcha_version", getRecaptchaVersion(form));
1200    form.submit();
1201    return;
1202  }
1203  const verified = await verifyRecaptchaBeforeContactSubmit(tokenEl.value, submitUrl, form);
1204  if (!verified) {
1205    alert("Security verification was rejected by the server. Please try submitting again.");
1206    return;
1207  }
1208  const params = new URLSearchParams();
1209  if (form.elements) {
1210    for (const el of form.elements) {
1211      if (!el.name || el.disabled) {
1212        continue;
1213      }
1214      if (isRecaptchaPostField(el.name)) {
1215        continue;
1216      }
1217      if (el.type === "submit" || el.type === "button" || el.type === "file") {
1218        continue;
1219      }
1220      if ((el.type === "checkbox" || el.type === "radio") && !el.checked) {
1221        continue;
1222      }
1223      params.set(el.name, el.value);
1224    }
1225  }
1226  // Session-miss fallback: server may re-verify token; version stays in PHP session from verify POST.
1227  params.set("g-recaptcha-response", tokenEl.value);
1228  const postForm = document.createElement("form");
1229  postForm.method = "POST";
1230  postForm.action = submitUrl;
1231  postForm.style.display = "none";
1232  params.forEach((value, key) => {
1233    const input = document.createElement("input");
1234    input.type = "hidden";
1235    input.name = key;
1236    input.value = value;
1237    postForm.appendChild(input);
1238  });
1239  document.body.appendChild(postForm);
1240  postForm.submit();
1241}
1242
1243function usesLegacyRecaptchaButtonClick() {
1244  return typeof window.buttonClick === "function" && /grecaptcha/.test(window.buttonClick.toString());
1245}
1246
1247function ensureConsentGatedButtonClick() {
1248  if (!pageHasRecaptchaMarkup()) {
1249    return false;
1250  }
1251  const captchaForms = findCaptchaProtectedForms();
1252  const hasCaptchaButtonClick = captchaForms.some(
1253    (form) => form.querySelector('[onclick*="buttonClick"]') || findExternalButtonClickTrigger(form)
1254  );
1255  const alreadyPatched =
1256    typeof window.buttonClick === "function"
1257    && /enableRecaptcha|activateRecaptcha|submitContactFormWithRecaptcha/.test(window.buttonClick.toString());
1258  const needsPatch = usesLegacyRecaptchaButtonClick()
1259    || (!alreadyPatched && hasCaptchaButtonClick);
1260  if (!needsPatch) {
1261    return false;
1262  }
1263  window.buttonClick = async function buttonClick() {
1264    const formEl = getRecaptchaForm();
1265    if (typeof checkRequired === "function" && formEl) {
1266      const missing = checkRequired(formEl);
1267      if (missing && missing.length) {
1268        return false;
1269      }
1270    }
1271    await submitContactFormWithRecaptcha(formEl);
1272  };
1273  return true;
1274}
1275
1276function usesLegacyInitiateCaptcha() {
1277  return typeof window.initiateCaptcha === "function"
1278    && /grecaptcha/.test(window.initiateCaptcha.toString());
1279}
1280
1281function ensureConsentGatedInitiateCaptcha() {
1282  if (!pageHasRecaptchaMarkup()) {
1283    return false;
1284  }
1285  const alreadyPatched =
1286    typeof window.initiateCaptcha === "function"
1287    && /submitContactFormWithRecaptcha/.test(window.initiateCaptcha.toString());
1288  const needsPatch = usesLegacyInitiateCaptcha()
1289    || (!alreadyPatched && document.querySelector('[onclick*="initiateCaptcha"]'));
1290  if (!needsPatch) {
1291    return false;
1292  }
1293  window.initiateCaptcha = async function initiateCaptcha() {
1294    const formEl = getRecaptchaForm();
1295    await submitContactFormWithRecaptcha(formEl);
1296  };
1297  return true;
1298}
1299
1300function formHasRecaptcha(form) {
1301  if (!form) {
1302    return false;
1303  }
1304  return Boolean(
1305    form.querySelector(".g-recaptcha")
1306    || form.querySelector("[data-cc-captcha]")
1307    || form.querySelector("[name='g-recaptcha-response']")
1308  );
1309}
1310
1311function formUsesButtonClickSubmit(form) {
1312  if (!form) {
1313    return false;
1314  }
1315  if (form.querySelector('[onclick*="buttonClick"]')) {
1316    return true;
1317  }
1318  if (findExternalButtonClickTrigger(form)) {
1319    return true;
1320  }
1321  return usesLegacyRecaptchaButtonClick();
1322}
1323
1324function ensureConsentGatedFormSubmit() {
1325  if (!pageHasRecaptchaMarkup()) {
1326    return false;
1327  }
1328  let gated = false;
1329  document.querySelectorAll("form").forEach((form) => {
1330    if (form.dataset.cookieRecaptchaSubmitGate) {
1331      return;
1332    }
1333    if (!formHasRecaptcha(form)) {
1334      return;
1335    }
1336    if (formUsesButtonClickSubmit(form)) {
1337      return;
1338    }
1339    form.dataset.cookieRecaptchaSubmitGate = "1";
1340    form.addEventListener("submit", async (event) => {
1341      event.preventDefault();
1342      event.stopPropagation();
1343      await submitContactFormWithRecaptcha(form);
1344    });
1345    gated = true;
1346  });
1347  return gated;
1348}
1349
1350function pageHasRecaptchaMarkup() {
1351  return Boolean(
1352    document.querySelector("[data-cc-captcha]")
1353    || document.getElementById("g-recaptcha-response")
1354    || document.querySelector("[name='g-recaptcha-response']")
1355    || document.querySelector(".g-recaptcha")
1356    || document.querySelector(".cookie-captcha-submit-notice")
1357  );
1358}
1359
1360function neutralizeLegacyRecaptchaScripts() {
1361  const hasMarkup = pageHasRecaptchaMarkup();
1362  const scripts = document.querySelectorAll(
1363    'script[src*="recaptcha/api.js"], script[src*="recaptcha/enterprise.js"]'
1364  );
1365  const inlineScripts = [...document.scripts].filter((script) =>
1366    /grecaptcha\.(?:render|execute|ready)/.test(script.textContent || "")
1367  );
1368  if (!hasMarkup) {
1369    return;
1370  }
1371  scripts.forEach((script) => {
1372    const type = (script.getAttribute("type") || "").toLowerCase();
1373    if (type === "text/plain" || script.getAttribute("data-cookie-recaptcha-loader")) {
1374      return;
1375    }
1376    const src = script.getAttribute("src");
1377    if (src) {
1378      script.setAttribute("data-cc-blocked-recaptcha-src", src);
1379      script.removeAttribute("src");
1380    }
1381    script.type = "text/plain";
1382  });
1383  inlineScripts.forEach((script) => {
1384    const type = (script.getAttribute("type") || "").toLowerCase();
1385    if (type === "text/plain") {
1386      return;
1387    }
1388    if (/function\s+buttonClick\s*\(/.test(script.textContent || "")) {
1389      return;
1390    }
1391    if (/function\s+initiateCaptcha\s*\(/.test(script.textContent || "")) {
1392      return;
1393    }
1394    script.remove();
1395  });
1396  if (window.grecaptcha && !isRecaptchaAccepted()) {
1397    delete window.grecaptcha;
1398    recaptchaScriptPromise = null;
1399    recaptchaScriptSrcLoaded = null;
1400  }
1401}
1402
1403function onConsentChange(userInitiated = false) {
1404  updateGtagConsent();
1405  updateWidgetPlaceholders();
1406  syncIframeManagerConsent();
1407  if (!userInitiated) {
1408    neutralizeLegacyRecaptchaScripts();
1409    return;
1410  }
1411  neutralizeLegacyRecaptchaScripts();
1412}
1413
1414
1415im.run({
1416  currLang: "en",
1417
1418  onChange: ({ changedServices, eventSource }) => {
1419    if (eventSource.type !== "click" || eventSource.action !== "accept") return;
1420
1421    const serviceCategory = {
1422    youtube: CAT_FUNCTIONALITY,
1423    vimeo: CAT_FUNCTIONALITY,
1424    googlemaps: CAT_FUNCTIONALITY,
1425    spotify: CAT_FUNCTIONALITY,
1426    soundcloud: CAT_FUNCTIONALITY,
1427    facebook_social: CAT_FUNCTIONALITY,
1428    investing_widget: CAT_ADVERTISEMENT,
1429    calendly: CAT_FUNCTIONALITY,
1430  };
1431
1432    const byCategory = {};
1433    for (const svc of changedServices) {
1434      const cat = serviceCategory[svc] || CAT_FUNCTIONALITY;
1435      (byCategory[cat] ||= []).push(svc);
1436    }
1437
1438    for (const [category, services] of Object.entries(byCategory)) {
1439      const prefs = CookieConsent.getUserPreferences();
1440      CookieConsent.acceptCategory([
1441        ...new Set([CAT_NECESSARY, ...(prefs.acceptedCategories || []), category]),
1442      ]);
1443      CookieConsent.acceptService(
1444        [...new Set([...(prefs.acceptedServices[category] || []), ...services])],
1445        category,
1446      );
1447    }
1448  },
1449
1450  services: {
1451    youtube: {
1452      embedUrl: "https://www.youtube-nocookie.com/embed/{data-id}",
1453      iframe: {
1454        allow:
1455          "accelerometer; encrypted-media; gyroscope; picture-in-picture; fullscreen;",
1456        loading: "lazy",
1457        frameborder: "0",
1458      },
1459      languages: {
1460        en: {
1461          notice:
1462            'This content is hosted by a third party. By showing the external 
1462content you accept the <a rel="noreferrer noopener" href="https://www.youtube.com/t/terms" target="_blank">terms and conditions</a> of youtube.com.',
1463          loadAllBtn: "Load video",
1464        },
1465      },
1466    },
1467
1468    vimeo: {
1469      embedUrl: "https://player.vimeo.com/video/{data-id}",
1470      iframe: {
1471        allow: "fullscreen; picture-in-picture;",
1472        loading: "lazy",
1473        frameborder: "0",
1474      },
1475      languages: {
1476        en: {
1477          notice:
1478            'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://vimeo.com/terms" target="_blank">terms and conditions</a> of vimeo.com.',
1479          loadAllBtn: "Load video",
1480        },
1481      },
1482    },
1483
1484    googlemaps: {
1485      embedUrl: "https://www.google.com/maps/{data-id}",
1486      iframe: {
1487        allow: "picture-in-picture; fullscreen;",
1488        loading: "lazy",
1489        referrerpolicy: "no-referrer-when-downgrade",
1490        frameborder: "0",
1491      },
1492      languages: {
1493        en: {
1494          notice:
1495            'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://cloud.google.com/maps-platform/terms" target="_blank">terms and conditions</a> of Google Maps.',
1496          loadAllBtn: "Load map",
1497        },
1498      },
1499    },
1500
1501    spotify: {
1502      embedUrl: "https://open.spotify.com/embed/{data-id}",
1503      iframe: {
1504        allow: "autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture;",
1505        loading: "lazy",
1506        frameborder: "0",
1507      },
1508      languages: {
1509        en: {
1510          notice:
1511            'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://www.spotify.com/legal/end-user-agreement/" target="_blank">terms and conditions</a> of Spotify.',
1512          loadAllBtn: "Load player",
1513        },
1514      },
1515    },
1516
1517    soundcloud: {
1518      embedUrl: "https://w.soundcloud.com/player/?{data-id}",
1519      iframe: {
1520        allow: "autoplay",
1521        loading: "lazy",
1522        frameborder: "0",
1523      },
1524      languages: {
1525        en: {
1526          notice:
1527            'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://soundcloud.com/terms-of-use" target="_blank">terms and conditions</a> of SoundCloud.',
1528          loadAllBtn: "Load player",
1529        },
1530      },
1531    },
1532
1533    facebook_social: {
1534      embedUrl: "https://www.facebook.com/plugins/{data-id}",
1535      iframe: {
1536        allow: "autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share",
1537        scrolling: "no",
1538        loading: "lazy",
1539        frameborder: "0",
1540      },
1541      languages: {
1542        en: {
1543          notice:
1544            'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://www.facebook.com/legal/terms" target="_blank">terms and conditions</a> of Facebook.',
1545          loadAllBtn: "Load feed",
1546        },
1547      },
1548    },
1549
1550    investing_widget: {
1551      embedUrl: "{data-id}",
1552      iframe: {
1553        scrolling: "no",
1554        loading: "lazy",
1555        frameborder: "0",
1556        allowtransparency: "true",
1557      },
1558      languages: {
1559        en: {
1560          notice:
1561            'This content is hosted by Investing.com. By showing the external content you accept their <a rel="noreferrer noopener" href="https://www.investing.com/about-us/terms-and-conditions" target="_blank">terms and conditions</a>.',
1562          loadAllBtn: "Load widget",
1563        },
1564      },
1565    },
1566
1567    calendly: {
1568      embedUrl: "https://calendly.com/{data-id}",
1569      iframe: {
1570        allow: "fullscreen; clipboard-write",
1571        loading: "lazy",
1572        frameborder: "0",
1573      },
1574      languages: {
1575        en: {
1576          notice:
1577            'This content is hosted by a third party. By showing the external content you accept the <a rel="noreferrer noopener" href="https://calendly.com/legal" target="_blank">terms and conditions</a> of Calendly.',
1578          loadAllBtn: "Load scheduler",
1579        },
1580      },
1581    },
1582  },
1583});
1584
1585CookieConsent.run({
1586  revision: 4,
1587
1588  page_scripts: true,
1589
1590  cookie: {
1591    secure: window.location.protocol === "https:",
1592  },
1593
1594  guiOptions: {
1595    consentModal: {
1596      layout: "box",
1597      position: "bottom right",
1598    },
1599    preferencesModal: {
1600      layout: "box",
1601    },
1602  },
1603
1604  onFirstConsent: () => {
1605    onConsentChange(true);
1606  },
1607  onConsent: () => {
1608    onConsentChange(false);
1609  },
1610  onChange: () => {
1611    onConsentChange(true);
1612  },
1613
1614  categories: {
1615    [CAT_NECESSARY]: {
1616      enabled: true,
1617      readOnly: true,
1618      services: {
1619        fontawesome: {
1620          label:
1621            "Loads Font Awesome icon fonts essential to site design.",
1622        },
1623        self_hosted_typography: {
1624          label:
1625            "Typography (Google Fonts) is self-hosted on our servers; no data is sent to Google for font delivery.",
1626        },
1627      },
1628    },
1629    [CAT_ANALYTICS]: {
1630      autoClear: {
1631        cookies: [
1632          {
1633            name: /^_ga/,
1634          },
1635          {
1636            name: "_gid",
1637          },
1638        ],
1639      },
1640      services: {
1641        [SERVICE_ANALYTICS_STORAGE]: {
1642          label:
1643            "Enables storage (such as cookies) related to analytics e.g. visit duration.",
1644        },
1645      },
1646    },
1647    [CAT_FUNCTIONALITY]: {
1648      services: {
1649        [SERVICE_FUNCTIONALITY_STORAGE]: {
1650          label:
1651            "Enables storage related to site functionality such as language preferences.",
1652        },
1653        youtube: {
1654          label:
1655            "Embeds YouTube videos, which may set cookies for playback and preferences.",
1656          onAccept: () => im.acceptService("youtube"),
1657          onReject: () => im.rejectService("youtube"),
1658        },
1659        vimeo: {
1660          label:
1661            "Embeds Vimeo videos, which may set cookies for playback and preferences.",
1662          onAccept: () => im.acceptService("vimeo"),
1663          onReject: () => im.rejectService("vimeo"),
1664        },
1665        googlemaps: {
1666          label: "Embeds an interactive Google Maps view of our location.",
1667          onAccept: () => im.acceptService("googlemaps"),
1668          onReject: () => {
1669            im.rejectService("googlemaps");
1670            document.querySelectorAll('iframe[src*="google.com/maps"], iframe[data-src*="google.com/maps"]').forEach((el) => el.remove());
1671            document.querySelectorAll('[data-service="googlemaps"]').forEach((el) => {
1672              if (el.querySelector("iframe")) {
1673                el.innerHTML = "";
1674              }
1675            });
1676          },
1677        },
1678        spotify: {
1679          label:
1680            "Embeds Spotify players, which may set cookies for playback and preferences.",
1681          onAccept: () => im.acceptService("spotify"),
1682          onReject: () => {
1683            im.rejectService("spotify");
1684            document.querySelectorAll('iframe[src*="spotify.com/embed"], iframe[data-src*="spotify.com/embed"]').forEach((el) => el.remove());
1685            document.querySelectorAll('[data-service="spotify"]').forEach((el) => {
1686              if (el.querySelector("iframe")) {
1687                el.innerHTML = "";
1688              }
1689            });
1690          },
1691        },
1692        soundcloud: {
1693          label:
1694            "Embeds SoundCloud players, which may set cookies for playback and preferences.",
1695          onAccept: () => im.acceptService("soundcloud"),
1696          onReject: () => {
1697            im.rejectService("soundcloud");
1698            document.querySelectorAll('iframe[src*="soundcloud.com/player"], iframe[data-src*="soundcloud.com/player"]').forEach((el) => el.remove());
1699            document.querySelectorAll('[data-service="soundcloud"]').forEach((el) => {
1700              if (el.querySelector("iframe")) {
1701                el.innerHTML = "";
1702              }
1703            });
1704          },
1705        },
1706        facebook_social: {
1707          label:
1708            "Enables Facebook social plugins such as like buttons and embedded posts.",
1709          onAccept: () => im.acceptService("facebook_social"),
1710          onReject: () => {
1711            im.rejectService("facebook_social");
1712            document.querySelectorAll('iframe[src*="facebook.com/plugins"], iframe[data-src*="facebook.com/plugins"]').forEach((el) => el.remove());
1713            document.querySelectorAll('[data-service="facebook_social"]').forEach((el) => {
1714              if (el.querySelector("iframe")) {
1715                el.innerHTML = "";
1716              }
1717            });
1718          },
1719        },
1720        calendly: {
1721          label:
1722            "Embeds Calendly scheduling, which may set cookies for appointment booking.",
1723          onAccept: () => im.acceptService("calendly"),
1724          onReject: () => {
1725            im.rejectService("calendly");
1726            document.querySelectorAll('iframe[src*="calendly.com"], iframe[data-src*="calendly.com"]').forEach((el) => el.remove());
1727            document.querySelectorAll('[data-service="calendly"]').forEach((el) => {
1728              if (el.querySelector("iframe")) {
1729                el.innerHTML = "";
1730              }
1731            });
1732          },
1733        },
1734      },
1735    },
1736    [CAT_SECURITY]: {
1737      services: {
1738        google_recaptcha: {
1739          label:
1740            "Loads Google reCAPTCHA to protect forms from spam and abuse.",
1741          onAccept: () => {
1742            updateCaptchaConsent();
1743            preloadRecaptchaOnConsent();
1744          },
1745        },
1746      },
1747    },
1748  },
1749
1750  language: {
1751    default: "en",
1752    translations: {
1753      en: {
1754        consentModal: {
1755          title: "We use cookies",
1756          description:
1757            "We use cookies for specific <strong>purposes</strong> - essential operation, analytics and functionality. Optional cookies may involve <strong>third parties</strong> such as Google; you can <strong>withdraw</strong> consent anytime via Manage preferences or Cookie Settings.",
1758          acceptAllBtn: "Accept all",
1759          acceptNecessaryBtn: "Reject all",
1760          showPreferencesBtn: "Manage preferences",
1761          footer: `
1762            <a href="${SITE_CONFIG.privacyPolicyUrl}">Privacy Policy</a>
1763            <a href="${SITE_CONFIG.contactUrl}">Contact</a>
1764          `,
1765        },
1766        preferencesModal: {
1767          title: "Manage cookie preferences",
1768          acceptAllBtn: "Accept all",
1769          acceptNecessaryBtn: "Reject all",
1770          savePreferencesBtn: "Accept current selection",
1771          closeIconLabel: "Close modal",
1772          sections: [
1773            {
1774              title: "Cookie usage",
1775              description:
1776                "We use cookies for specific purposes: essential site operation, analytics, and enhanced functionality. When you enable optional categories, data may be processed by third-parties such as Google, and embedded content providers. Self-hosted typography, Font Awesome load typography essential to our site design. See our privacy policy for full details.",
1777            },
1778            {
1779              title: "Strictly necessary cookies",
1780              description:
1781                "These cookies and services are essential for the proper functioning of the website, for example for user authentication. Typography (Google Fonts) is self-hosted on our servers and does not send data to Google. Font Awesome loads icon fonts essential to site design and is treated as strictly necessary.",
1782              linkedCategory: CAT_NECESSARY,
1783            },
1784            {
1785              title: "Analytics",
1786              description:
1787                "Cookies used for analytics help collect data that allows services to understand how users interact with a particular service. Data may be shared with third-parties such as Google Analytics.",
1788              linkedCategory: CAT_ANALYTICS,
1789              cookieTable: {
1790                headers: {
1791                  name: "Name",
1792                  domain: "Service",
1793                  description: "Description",
1794                  expiration: "Expiration",
1795                },
1796                body: [
1797                  {
1798                    name: "_ga",
1799                    domain: "Google Analytics",
1800                    description:
1801                      "Distinguishes unique users via a randomly generated client ID.",
1802                    expiration: "2 years",
1803                  },
1804                  {
1805                    name: "_ga_M3GFFYS5D0",
1806                    domain: "Google Analytics 4",
1807                    description:
1808                      "Persists session state for a GA4 property.",
1809                    expiration: "2 years",
1810                  },
1811                  {
1812                    name: "_ga_PYXQQYK45B",
1813                    domain: "Google Analytics 4",
1814                    description:
1815                      "Persists session state for a GA4 property.",
1816                    expiration: "2 years",
1817                  },
1818                  {
1819                    name: "_gat_gtag_UA_215523884_8",
1820                    domain: "Google Analytics",
1821                    description:
1822                      "Throttles request rate to Google Analytics.",
1823                    expiration: "1 minute",
1824                  },
1825                  {
1826                    name: "_gid",
1827                    domain: "Google Analytics",
1828                    description:
1829                      "Distinguishes users for the current day.",
1830                    expiration: "24 hours",
1831                  },
1832                ],
1833              },
1834            },
1835            {
1836              title: "Functionality",
1837              description:
1838                "Cookies used for functionality enable enhanced features such as embedded YouTube, Vimeo, Google Maps, Spotify, and SoundCloud content. These third-parties may set their own cookies when enabled.",
1839              linkedCategory: CAT_FUNCTIONALITY,
1840            },
1841            {
1842              title: "Security",
1843              description:
1844                "Security verification (Google reCAPTCHA) loads only when you enable it for protected forms.",
1845              linkedCategory: CAT_SECURITY,
1846            },
1847            {
1848              title: "More information",
1849              description: `Read our <a href="${SITE_CONFIG.privacyPolicyUrl}">Privacy Policy</a> for full details on purposes, third-parties, and data retention. You may withdraw or change your consent at any time using Manage preferences above or the Cookie Settings link in our site footer. For other queries, <a href="${SITE_CONFIG.contactUrl}">contact us</a>.`,
1850            },
1851          ],
1852        },
1853      },
1854    },
1855  },
1856});
1857
1858window.showCookiePreferences = () => CookieConsent.showPreferences();
1859updateWidgetPlaceholders();
1860neutralizeLegacyRecaptchaScripts();
1861ensureCaptchaMetadata();
1862ensureCaptchaSubmitNotice();
1863ensureConsentGatedButtonClick();
1864ensureConsentGatedInitiateCaptcha();
1865ensureConsentGatedFormSubmit();
1866updateCaptchaConsent();
1867window.enableRecaptcha = enableRecaptcha;
1868window.activateRecaptcha = activateRecaptcha;
1869window.isRecaptchaAccepted = isRecaptchaAccepted;
1870window.submitContactFormWithRecaptcha = submitContactFormWithRecaptcha;
1871window.ensureConsentGatedButtonClick = ensureConsentGatedButtonClick;
1872window.ensureConsentGatedInitiateCaptcha = ensureConsentGatedInitiateCaptcha;
1873window.ensureConsentGatedFormSubmit = ensureConsentGatedFormSubmit;
1874new MutationObserver(() => {
1875  neutralizeLegacyRecaptchaScripts();
1876  ensureCaptchaMetadata();
1877  ensureCaptchaSubmitNotice();
1878  ensureConsentGatedButtonClick();
1879  ensureConsentGatedInitiateCaptcha();
1880  ensureConsentGatedFormSubmit();
1881}).observe(
1882  document.documentElement,
1883  { childList: true, subtree: true }
1884);
1885
1886function bootstrapCookieConsentFromIm() {
1887  const serviceCategory = {
1888    youtube: CAT_FUNCTIONALITY,
1889    vimeo: CAT_FUNCTIONALITY,
1890    googlemaps: CAT_FUNCTIONALITY,
1891    spotify: CAT_FUNCTIONALITY,
1892    soundcloud: CAT_FUNCTIONALITY,
1893    facebook_social: CAT_FUNCTIONALITY,
1894    investing_widget: CAT_ADVERTISEMENT,
1895    calendly: CAT_FUNCTIONALITY,
1896  };
1897  const imAccepted = im.getState().acceptedServices || [];
1898  for (const svc of imAccepted) {
1899    const category = serviceCategory[svc] || CAT_FUNCTIONALITY;
1900    if (widgetConsentGranted(svc, category)) continue;
1901    const prefs = CookieConsent.getUserPreferences();
1902    CookieConsent.acceptCategory([
1903      ...new Set([CAT_NECESSARY, ...(prefs.acceptedCategories || []), category]),
1904    ]);
1905    CookieConsent.acceptService(
1906      [...new Set([...(prefs.acceptedServices[category] || []), svc])],
1907      category,
1908    );
1909  }
1910}
1911bootstrapCookieConsentFromIm();
1912syncIframeManagerConsent();

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.