PageSourceSearch

https://7mountainsmedia.com/wp-content/plugins/universal-consent-p…=0.1.33-alpha.185369.2c4d1b63.1789048956

js 7mountainsmedia.com collected 2026-10-02 14:34:12 UTC 185,369 bytes, 5,465 lines download raw bytes

1/**
2 * Consent surface guard — captcha forms, maps, and gated embeds.
3 * When the required category is denied, show a theme-matched blocking notice
4 * so visitors do not interact with a broken surface (or lose form input).
5 */
6(function () {
7  'use strict';
8
9  if (window.__ucpfConsentSurfaceGuard) {
10    return;
11  }
12  window.__ucpfConsentSurfaceGuard = true;
13  // Legacy flag used by earlier builds.
14  window.__ucpfCaptchaGuard = true;
15
16  /**
17   * When true: skip Elementor Motion FX / sticky layout recovery hacks only.
18   * Consent overlays, captcha covers, and video parking ALWAYS run on every
19   * builder (Elementor, Divi, Gutenberg, Bricks, etc.) — GDPR surfaces are not optional.
20   */
21  function leaveBuildersAlone() {
22    var c = window.ucpfConfig || {};
23    return c.leaveBuildersAlone !== false;
24  }
25
26  function isBuilderChrome(node) {
27    if (!node || node.nodeType !== 1) {
28      return false;
29    }
30    try {
31      if (node.closest) {
32        return !!(
33          node.closest('.elementor') ||
34          node.closest('.elementor-element') ||
35          node.closest('.elementor-widget') ||
36          node.closest('[data-elementor-type]') ||
37          node.closest('.e-con') ||
38          node.closest('.et_pb_section') ||
39          node.closest('.et_pb_module') ||
40          node.closest('.brxe-section') ||
41          node.closest('.fl-builder-content') ||
42          node.closest('.oxygen-body') ||
43          node.closest('.breakdance')
44        );
45      }
46    } catch (eB) {}
47    return false;
48  }
49
50  var TOKEN_PROPS = [
51    '--ucpf-black',
52    '--ucpf-surface',
53    '--ucpf-cream',
54    '--ucpf-muted',
55    '--ucpf-border',
56    '--ucpf-accent',
57    '--ucpf-accent-hover',
58    '--ucpf-accent-active',
59    '--ucpf-accent-2',
60    '--ucpf-on-accent',
61    '--ucpf-label',
62    '--ucpf-focus',
63    '--ucpf-focus-ring',
64    '--ucpf-shadow-panel',
65    '--ucpf-font-sans',
66    '--ucpf-radius-soft',
67  ];
68
69  /**
70   * Provider-first markers (any theme/plugin), then popular WP builder wrappers.
71   * Broad [class*="…"] / [name*="…"] patterns are only used via form heuristic
72   * (see formHasCaptchaSignal) to avoid guarding non-form page widgets.
73   */
74  var CAPTCHA_MARKERS = [
75    // —— Cloudflare Turnstile ——
76    '.cf-turnstile',
77    '#cf-turnstile',
78    '.cf-turnstile-wrapper',
79    '[data-turnstile]',
80    '[name="cf-turnstile-response"]',
81    '[name*="cf-turnstile"]',
82    'iframe[src*="challenges.cloudflare.com"]',
83    'iframe[data-src*="challenges.cloudflare.com"]',
84    'iframe[src*="turnstile"]',
85    'iframe[data-src*="turnstile"]',
86    // —— Google reCAPTCHA ——
87    '.g-recaptcha',
88    '.grecaptcha-badge',
89    '.grecaptcha-logo',
90    '#g-recaptcha-response',
91    'textarea[name="g-recaptcha-response"]',
92    '[name*="g-recaptcha"]',
93    '[id*="g-recaptcha"]',
94    'iframe[src*="recaptcha"]',
95    'iframe[data-src*="recaptcha"]',
96    'iframe[src*="google.com/recaptcha"]',
97    'iframe[data-src*="google.com/recaptcha"]',
98    'iframe[title*="reCAPTCHA"]',
99    'iframe[title*="recaptcha"]',
100    // Empty widget hosts (Forminator / Elementor / custom)
101    'div[data-sitekey][data-size]',
102    'div[data-sitekey][data-theme]',
103    'div[data-sitekey][data-callback]',
104    // —— hCaptcha ——
105    '.h-captcha',
106    '[name="h-captcha-response"]',
107    '[name*="h-captcha"]',
108    'iframe[src*="hcaptcha.com"]',
109    'iframe[data-src*="hcaptcha.com"]',
110    'iframe[src*="newassets.hcaptcha.com"]',
111    'iframe[title*="hCaptcha"]',
112    'iframe[title*="hcaptcha"]',
113    // —— Friendly Captcha ——
114    '.frc-captcha',
115    '[class*="frc-captcha"]',
116    '[name*="frc-"]',
117    'iframe[src*="friendlycaptcha"]',
118    'iframe[data-src*="friendlycaptcha"]',
119    // —— Gravity Forms ——
120    '.ginput_recaptcha',
121    '.gform_recaptcha',
122    '.gfield--type-captcha',
123    '.ginput_container_captcha',
124    '.gform_captcha',
125    '.gform_captcha_button',
126    '.gform_turnstile',
127    '.gf-cloudflare-turnstile',
128    '.cf-turnstile-response',
129    '.gform_wrapper .gfield--type-captcha',
130    'form[id^="gform_"] .gfield--type-captcha',
131    'form[id^="gform_"] [data-sitekey]',
132    // —— Forminator ——
133    '.forminator-g-recaptcha',
134    '.forminator-hcaptcha',
135    '.forminator-captcha',
136    '.forminator-field-captcha',
137    '.forminator-captcha-left',
138    '.forminator-captcha-right',
139    '[class*="forminator"][class*="recaptcha"]',
140    '[class*="forminator"][class*="hcaptcha"]',
141    '[class*="forminator"][class*="turnstile"]',
142    // —— WPForms / Fluent / CF7 / Ninja ——
143    '.wpforms-recaptcha-container',
144    '.wpforms-is-recaptcha',
145    '.wpforms-captcha-invisible',
146    '.ff-el-recaptcha',
147    '.ff-el-hcaptcha',
148    '.wpcf7-form-control.g-recaptcha',
149    '.wpcf7-recaptcha',
150    '.nf-field .g-recaptcha',
151    // —— Elementor Form ——
152    '.elementor-field-type-recaptcha',
153    '.elementor-field-type-recaptcha_v3',
154    '.elementor-g-recaptcha',
155    // —— Formidable / Quform / HappyForms / JetFormBuilder ——
156    '.frm-g-recaptcha',
157    '[id*="frm_field"][class*="captcha"]',
158    '.frm_form_field[class*="captcha"]',
159    '.quform-captcha',
160    '.quform-recaptcha',
161    '.happyforms-part--recaptcha',
162    '.happyforms-part--captcha',
163    '[class*="jet-form"][class*="recaptcha"]',
164    '[class*="jet-form"][class*="hcaptcha"]',
165    '[class*="jet-form"][class*="turnstile"]',
166    // —— Kadence / Spectra / Bricks / Divi ——
167    '[class*="kb-recaptcha"]',
168    '[class*="uagb"][class*="recaptcha"]',
169    '[class*="brxe"][class*="recaptcha"]',
170    '.et_pb_contact_captcha',
171    // —— WooCommerce captcha plugins ——
172    '[class*="woo"][class*="recaptcha"]',
173    '[class*="woo"][class*="hcaptcha"]',
174    '[class*="woo"][class*="turnstile"]',
175    // —— Simple Cloudflare Turnstile / BestWebSoft Google Captcha ——
176    '[class*="cf-turnstile"]',
177    '.gglcptch',
178    '#gglcptch_recaptcha',
179    // —— Amelia Booking (captcha often injected on payment step) ——
180    '#amelia-container .g-recaptcha',
181    '#amelia-container .cf-turnstile',
182    '#amelia-container .h-captcha',
183    '.am-fs__wrapper .g-recaptcha',
184    '.am-fs__wrapper .cf-turnstile',
185    '.am-fs__wrapper .h-captcha',
186    // —— UCPF-blocked security scripts ——
187    'script[data-ucpf-category="security"][src*="recaptcha"]',
188    'script[data-ucpf-category="security"][src*="hcaptcha"]',
189    'script[data-ucpf-category="security"][src*="challenges.cloudflare.com"]',
190    'script[data-ucpf-category="security"][src*="friendlycaptcha"]',
191    'script[type="text/plain"][data-src*="recaptcha"]',
192    'script[type="text/plain"][data-src*="hcaptcha"]',
193    'script[type="text/plain"][data-src*="challenges.cloudflare.com"]',
194    'script[type="text/plain"][data-src*="friendlycaptcha"]',
195  ];
196
197  /** Broad patterns only scanned inside <form> (heuristic pass). */
198  var CAPTCHA_FORM_INNER_SELECTORS = [
199    '[class*="g-recaptcha"]',
200    '[class*="grecaptcha"]',
201    '[class*="h-captcha"]',
202    '[class*="hcaptcha"]',
203    '[class*="turnstile"]',
204    '[class*="recaptcha"]',
205    '[class*="frc-captcha"]',
206    '[data-sitekey]',
207  ];
208
209  var MAP_MARKERS = [
210    '.wpgmza_map',
211    '.wpgmza_map_container',
212    '.gm-style',
213    '.mapboxgl-map',
214    '.maplibregl-map',
215    '.leaflet-container',
216    '.mapster-wp-maps-container',
217    '.mapster-wp-maps',
218    '.mapster-map',
219    '[id^="mapster-wp-maps"]',
220    '[data-mapbox-map]',
221    '.elementor-widget-google_maps',
222    '[data-widget_type="google_maps.default"]',
223    '[data-widget_type*="google_maps"]',
224    'gmp-map',
225    'gmp-advanced-marker',
226    'iframe[src*="google.com/maps"]',
227    'iframe[data-src*="google.com/maps"]',
228    'iframe[src*="maps.google"]',
229    'iframe[data-src*="maps.google"]',
230    'iframe[src*="openstreetmap.org"]',
231    'iframe[data-src*="openstreetmap.org"]',
232    'iframe[src*="mapbox.com"]',
233    'iframe[data-src*="mapbox.com"]',
234    'iframe[src*="mapme.com"]',
235    'iframe[data-src*="mapme.com"]',
236    'iframe[data-lazy-src*="mapme.com"]',
237    'iframe[src*="maphub.net"]',
238    'iframe[data-src*="maphub.net"]',
239    'iframe[src*="batchgeo.com"]',
240    'iframe[data-src*="batchgeo.com"]',
241    'iframe[src*="zeemaps.com"]',
242    'iframe[data-src*="zeemaps.com"]',
243    'iframe[src*="umap.openstreetmap.fr"]',
244    'iframe[data-src*="umap.openstreetmap.fr"]',
245    'iframe[src*="arcgis.com"]',
246    'iframe[data-src*="arcgis.com"]',
247    'iframe[src*="bing.com/maps"]',
248    'iframe[data-src*="bing.com/maps"]',
249  ];
250
251  /** Calendly scheduling embeds (inline widgets + iframes; Elementor popups inject late). */
252  var CALENDLY_MARKERS = [
253    '.calendly-inline-widget',
254    '.calendly-badge-widget',
255    '.calendly-overlay',
256    '[data-url*="calendly.com"]',
257    'iframe[src*="calendly.com"]',
258    'iframe[data-src*="calendly.com"]',
259    'iframe[data-lazy-src*="calendly.com"]',
260  ];
261
262  /** Third-party booking / work-request / form embeds (Jobber, Typeform, …). */
263  var WIDGET_EMBED_MARKERS = [
264    '.jobber-inline-work-request',
265    'iframe.jobber-work-request',
266    'iframe[src*="getjobber.com"]',
267    'iframe[data-src*="getjobber.com"]',
268    'iframe[src*="clienthub.getjobber.com"]',
269    'iframe[data-src*="clienthub.getjobber.com"]',
270    'iframe[src*="typeform.com"]',
271    'iframe[data-src*="typeform.com"]',
272    'iframe[src*="jotform.com"]',
273    'iframe[data-src*="jotform.com"]',
274    'iframe[src*="forms.hsforms.com"]',
275    'iframe[data-src*="forms.hsforms.com"]',
276    'iframe[src*="tally.so"]',
277    'iframe[data-src*="tally.so"]',
278  ];
279
280  /** Amelia Booking — first-party WP booking UI (like Gravity Forms). Overlay only. */
281  var AMELIA_FORM_MARKERS = [
282    '#amelia-container',
283    '.am-fs__wrapper',
284  ];
285
286  /** Smash Balloon YouTube Feed Pro — shortcode roots (not the #youtube-feed-loader spinner). */
287  var YOUTUBE_FEED_MARKERS = [
288    '.sb_youtube',
289    '[id^="sb_youtube"]',
290    '#regular-youtube-container',
291    '#live-youtube-container',
292  ];
293
294  /** Smash Balloon Social Wall (Instagram / mixed social). */
295  var SOCIAL_WALL_MARKERS = [
296    '.sb-wall',
297    '[id^="sb-wall"]',
298  ];
299
300  var VIDEO_MARKERS = [
301    'iframe[src*="youtube.com"]',
302    'iframe[data-src*="youtube.com"]',
303    'iframe[data-lazy-src*="youtube"]',
304    'iframe[src*="youtu.be"]',
305    'iframe[data-src*="youtu.be"]',
306    'iframe[src*="youtube-nocookie.com"]',
307    'iframe[data-src*="youtube-nocookie.com"]',
308    'iframe[src*="player.vimeo.com"]',
309    'iframe[data-src*="player.vimeo.com"]',
310    'iframe[data-lazy-src*="vimeo"]',
311    'iframe[src*="vimeo.com"]',
312    'iframe[data-src*="vimeo.com"]',
313    // Lazy / CMP-style attrs — filtered by isVisualEmbedSurface (skips <script> / *.js APIs).
314    '[data-src*="youtube.com"]',
315    '[data-src*="youtu.be"]',
316    '[data-src*="youtube-nocookie.com"]',
317    '[data-src*="player.vimeo.com/video"]',
318    '[data-src*="vimeo.com/video"]',
319    '[data-lazy-src*="youtube"]',
320    '[data-lazy-src*="vimeo"]',
321    '[data-youtube]',
322    '[data-youtube-url]',
323    '[data-vimeo-url]',
324    '[data-vimeo-id]',
325  ];
326
327  /** Builder / theme video shells that inject iframes later (often empty at paint). */
328  var VIDEO_SHELL_SELECTORS = [
329    // Elementor
330    '.elementor-widget-video',
331    '[data-widget_type="video.default"]',
332    '[data-widget_type="video"]',
333    '.elementor-video',
334    '.elementor-wrapper.elementor-open-inline',
335    '.elementor-background-video-container',
336    '.elementor-background-video-embed',
337    // Gutenberg
338    '.wp-block-embed-youtube',
339    '.wp-block-embed-vimeo',
340    '.wp-block-embed.is-provider-youtube',
341    '.wp-block-embed.is-provider-vimeo',
342    '.wp-block-embed.is-type-video',
343    '.wp-block-video',
344    // Divi
345    '.et_pb_video',
346    '.et_pb_video_box',
347    '.et_pb_slide_video',
348    // WPBakery / Visual Composer
349    '.wpb_video_widget',
350    '.wpb_video_wrapper',
351    '.vc_video-bg',
352    // Bricks
353    '.brxe-video',
354    '[data-script-id][class*="brxe-video"]',
355    // Beaver
356    '.fl-module-video',
357    '.fl-video',
358    // Oxygen / Breakdance-ish
359    '.oxy-video',
360    '.breakdance-video',
361    // Theme / generic oEmbed wrappers
362    '.wp-block-embed__wrapper',
363    '.fluidvids',
364    '.rll-youtube-player',
365    '.wp-video',
366    'lite-youtube',
367    'lite-vimeo',
368  ];
369
370  /**
371   * WooCommerce / checkout surfaces that need Embeds & Widgets (functional)
372   * for PayPal, Stripe, Square, and similar payment widgets.
373   */
374  var CHECKOUT_SELECTORS = [
375    'form.woocommerce-checkout',
376    'form.checkout',
377    'form#order_review',
378    '.woocommerce-checkout',
379    '.woocommerce-order-pay',
380    '#payment',
381    '.wc-block-checkout',
382    '.wp-block-woocommerce-checkout',
383    '.wc-block-cart__submit-container',
384  ];
385
386  /** @type {WeakMap<Element, string>} */
387  var guarded = new WeakMap();
388
389  function t(key, fallback) {
390    var i18n = (window.ucpfConfig && window.ucpfConfig.i18n) || {};
391    return i18n[key] || fallback;
392  }
393
394  function categoryLabel(slug) {
395    var cats = (window.ucpfConfig && window.ucpfConfig.categories) || {};
396    if (cats[slug] && cats[slug].label) {
397      return cats[slug].label;
398    }
399    return slug.charAt(0).toUpperCase() + slug.slice(1);
400  }
401
402  function hasCategoryConsent(slug) {
403    if (window.__ucpfDiscover) {
404      return true;
405    }
406    if (!slug || slug === 'necessary') {
407      return true;
408    }
409    if (window.UCPF && typeof window.UCPF.hasConsent === 'function') {
410      return !!window.UCPF.hasConsent(slug);
411    }
412    try {
413      var handoff = window.__ucpfConsentHandoff;
414      if (handoff && handoff.categories && handoff.categories[slug]) {
415        return true;
416      }
417    } catch (eHandoff) { /* ignore */ }
418    try {
419      var match = document.cookie.match(/(?:^|; )ucpf_consent=([^;]*)/);
420      if (match) {
421        var data = JSON.parse(decodeURIComponent(match[1]));
422        if (data && data.categories && data.categories[slug]) {
423          return true;
424        }
425      }
426    } catch (e) { /* ignore */ }
427    // Safari may lose the cookie across reload; honor storage backup / bridge.
428    try {
429      var cfg = window.ucpfConfig || {};
430      var suffix = (cfg.storageSuffix || '').toString().replace(/[^a-zA-Z0-9_-]/g, '');
431      var keys = [];
432      if (suffix) {
433        keys.push('ucpf_consent_backup_' + suffix, 'ucpf_consent_bridge_' + suffix);
434      }
435      keys.push('ucpf_consent_backup', 'ucpf_consent_bridge');
436      for (var i = 0; i < keys.length; i++) {
437        var raw = null;
438        if (window.localStorage) {
439          raw = localStorage.getItem(keys[i]);
440        }
441        if (!raw && window.sessionStorage) {
442          raw = sessionStorage.getItem(keys[i]);
443        }
444        if (!raw) {
445          continue;
446        }
447        var parsed = JSON.parse(raw);
448        if (parsed && parsed.categories && parsed.categories[slug]) {
449          return true;
450        }
451      }
452    } catch (e2) { /* ignore */ }
453    return false;
454  }
455
456  function bannerTheme() {
457    var cfg = window.ucpfConfig || {};
458    var theme = cfg.bannerTheme || 'classic';
459    return String(theme).replace(/[^a-z0-9_]/gi, '') || 'classic';
460  }
461
462  function applyTokenMap(el, map) {
463    if (!el || !map) {
464      return;
465    }
466    Object.keys(map).forEach(function (prop) {
467      var v = map[prop];
468      if (v === undefined || v === null || v === '') {
469        return;
470      }
471      var name = prop.indexOf('--') === 0 ? prop : '--' + prop;
472      el.style.setProperty(name, String(v));
473    });
474  }
475
476  function syncThemeOnto(el) {
477    if (!el) {
478      return;
479    }
480    var theme = bannerTheme();
481    Array.prototype.slice.call(el.classList).forEach(function (cls) {
482      if (cls.indexOf('ucpf-theme-') === 0) {
483        el.classList.remove(cls);
484      }
485    });
486    el.classList.add('ucpf-theme-' + theme);
487
488    // 1) Authoritative tokens from PHP (active preset + Banner & Branding customs).
489    var cfg = window.ucpfConfig || {};
490    if (cfg.themeTokens && typeof cfg.themeTokens === 'object') {
491      applyTokenMap(el, cfg.themeTokens);
492    }
493
494    // 2) Live refine from #ucpf-root when present (keeps guard identical to banner).
495    var root = document.getElementById('ucpf-root');
496    if (root && window.getComputedStyle) {
497      var cs = window.getComputedStyle(root);
498      TOKEN_PROPS.forEach(function (prop) {
499        var v = cs.getPropertyValue(prop);
500        if (v && String(v).trim()) {
501          el.style.setProperty(prop, String(v).trim());
502        }
503      });
504      // Also copy any other --ucpf-* the theme may set.
505      try {
506        for (var i = 0; i < cs.length; i++) {
507          var name = cs[i];
508          if (name && name.indexOf('--ucpf-') === 0) {
509            var val = cs.getPropertyValue(name);
510            if (val && String(val).trim()) {
511              el.style.setProperty(name, String(val).trim());
512            }
513          }
514        }
515      } catch (e) {
516        /* older browsers */
517      }
518    }
519  }
520
521  function resyncAllGuards() {
522    var nodes = document.querySelectorAll('.ucpf-consent-guard, .ucpf-captcha-guard');
523    Array.prototype.forEach.call(nodes, syncThemeOnto);
524  }
525
526  function findFormForNode(node) {
527    if (!node) {
528      return null;
529    }
530    if (node.tagName === 'FORM') {
531      return node;
532    }
533    if (node.closest) {
534      var form =
535        node.closest('form') ||
536        node.closest('form.forminator-custom-form') ||
537        node.closest('form.forminator-ui') ||
538        node.closest('form.wpforms-form') ||
539        node.closest('form.wpcf7-form') ||
540        node.closest('form.frm-fluent-form') ||
541        node.closest('form.nf-form-content') ||
542        node.closest('form.elementor-form') ||
543        node.closest('form.frm-show-form') ||
544        node.closest('form.formidable') ||
545        node.closest('form.quform-form') ||
546        node.closest('form.happyforms-form') ||
547        node.closest('form.jet-form-builder') ||
548        node.closest('form.kb-form') ||
549        node.closest('form.et_pb_contact_form');
550      if (form) {
551        return form;
552      }
553      // Captcha sometimes sits in a wrapper just outside / beside the form markup.
554      var shell =
555        node.closest('.gform_wrapper') ||
556        node.closest('.gform_body') ||
557        node.closest('.forminator-custom-form') ||
558        node.closest('.elementor-shortcode') ||
559        node.closest('.wpforms-container') ||
560        node.closest('.wpcf7') ||
561        node.closest('.elementor-widget-form') ||
562        node.closest('.frm_forms') ||
563        node.closest('.formidable') ||
564        node.closest('.quform-form-wrap') ||
565        node.closest('.happyforms-form') ||
566        node.closest('.jet-form-builder') ||
567        node.closest('.kb-form') ||
568        node.closest('.et_pb_contact_form');
569      if (shell) {
570        var nested =
571          shell.querySelector('form') ||
572          (shell.tagName === 'FORM' ? shell : null) ||
573          (shell.id && String(shell.id).indexOf('gform_') === 0 ? shell : null);
574        if (nested) {
575          return nested;
576        }
577        // GF: cover the wrapper when the <form> node is missing / replaced mid-AJAX.
578        if (shell.classList && shell.classList.contains('gform_wrapper')) {
579          return shell;
580        }
581      }
582    }
583    var parent = node.parentElement;
584    for (var i = 0; parent && i < 8; i++) {
585      if (parent.tagName === 'FORM') {
586        return parent;
587      }
588      parent = parent.parentElement;
589    }
590    return null;
591  }
592
593  function queryAll(sels) {
594    var out = [];
595    var seen = [];
596    sels.forEach(function (sel) {
597      var nodes;
598      try {
599        nodes = document.querySelectorAll(sel);
600      } catch (e) {
601        return;
602      }
603      Array.prototype.forEach.call(nodes, function (node) {
604        for (var i = 0; i < seen.length; i++) {
605          if (seen[i] === node) {
606            return;
607          }
608        }
609        seen.push(node);
610        out.push(node);
611      });
612    });
613    return out;
614  }
615
616  function lockFields(form) {
617    var controls = form.querySelectorAll('input, textarea, select, button');
618    Array.prototype.forEach.call(controls, function (el) {
619      if (el.getAttribute('data-ucpf-captcha-skip') === '1') {
620        return;
621      }
622      if (!el.hasAttribute('data-ucpf-captcha-prev-disabled')) {
623        el.setAttribute('data-ucpf-captcha-prev-disabled', el.disabled ? '1' : '0');
624      }
625      if (!el.hasAttribute('data-ucpf-captcha-prev-readonly') && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) {
626        el.setAttribute('data-ucpf-captcha-prev-readonly', el.readOnly ? '1' : '0');
627      }
628      if (el.tagName === 'BUTTON' || (el.tagName === 'INPUT' && /^(submit|button|image)$/i.test(el.type))) {
629        el.disabled = true;
630      } else if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
631        el.readOnly = true;
632        el.disabled = true;
633      } else {
634        el.disabled = true;
635      }
636    });
637    form.setAttribute('aria-disabled', 'true');
638  }
639
640  function unlockFields(form) {
641    var controls = form.querySelectorAll('[data-ucpf-captcha-prev-disabled], [data-ucpf-captcha-prev-readonly]');
642    Array.prototype.forEach.call(controls, function (el) {
643      var wasDisabled = el.getAttribute('data-ucpf-captcha-prev-disabled');
644      var wasReadonly = el.getAttribute('data-ucpf-captcha-prev-readonly');
645      if (wasDisabled !== null) {
646        el.disabled = wasDisabled === '1';
647        el.removeAttribute('data-ucpf-captcha-prev-disabled');
648      }
649      if (wasReadonly !== null) {
650        el.readOnly = wasReadonly === '1';
651        el.removeAttribute('data-ucpf-captcha-prev-readonly');
652      }
653    });
654    form.removeAttribute('aria-disabled');
655  }
656
657  function copyForKind(kind, category, categories) {
658    var label = categoryLabel(category);
659    var cats = Array.isArray(categories) && categories.length ? categories : category ? [category] : [];
660    if (kind === 'checkout') {
661      var needsSec = cats.indexOf('security') !== -1;
662      var needsFun = cats.indexOf('functional') !== -1;
663      if (needsSec && needsFun) {
664        return {
665          title: t('checkoutGuardCombinedTitle', 'Checkout needs Security & Embeds'),
666          body: t(
667            'checkoutGuardCombinedBody',
668            'Checkout uses anti-spam protection (CAPTCHA) and payment / shipping widgets. Enable Security and Embeds & Widgets together before entering details so nothing is lost.'
669          ),
670          enable: t('checkoutGuardCombinedEnable', 'Enable required cookies & continue'),
671          categories: cats,
672        };
673      }
674      return {
675        title: t('checkoutGuardTitle', 'Checkout needs Embeds & Widgets'),
676        body: t(
677          'checkoutGuardBody',
678          'Payment and shipping widgets (PayPal, Stripe, Square, Shippo, UPS, USPS, and similar) need Embeds & Widgets cookies before checkout can request rates, validate addresses, or load payment buttons. Enable them before entering details so nothing is lost.'
679        ),
680        enable: t('checkoutGuardEnable', 'Enable Embeds & Widgets & continue'),
681        categories: cats.length ? cats : ['functional'],
682      };
683    }
684    if (kind === 'captcha') {
685      return {
686        title: t('captchaGuardTitle', 'CAPTCHA required before you can use this form'),
687        body: t(
688          'captchaGuardBody',
689          'This form uses anti-spam protection (CAPTCHA / Turnstile) that needs Security cookies. Enable Security before filling fields so your entries are not lost.'
690        ),
691        enable: t('captchaGuardEnable', 'Enable Security & continue'),
692        categories: cats.length ? cats : ['security'],
693      };
694    }
695    if (kind === 'map') {
696      return {
697        title: t('embedGuardMapTitle', 'Map blocked until you allow Marketing & Embeds'),
698        body: t(
699          'embedGuardMapBody',
700          'This map loads third-party content we cannot fully control. Enable Marketing and Embeds & Widgets so it can load.'
701        ),
702        enable: t('embedGuardEnableVideo', 'Enable Marketing & Embeds & continue'),
703        categories: ensureEmbedConsentCategories(cats),
704      };
705    }
706    // YouTube / Vimeo: marketing pixels + embed players often need both.
707    if (kind === 'youtube' || kind === 'vimeo') {
708      var videoCats = ensureEmbedConsentCategories(cats);
709      return {
710        title: t('embedGuardVideoTitle', 'Video blocked until you allow Marketing & Embeds'),
711        body: t(
712          'embedGuardVideoBody',
713          'This embedded video needs Marketing and Embeds & Widgets cookies. Enable both to load the player.'
714        ),
715        enable: t('embedGuardEnableVideo', 'Enable Marketing & Embeds & continue'),
716        categories: videoCats,
717      };
718    }
719    if (kind === 'embed' && category === 'marketing') {
720      return {
721        title: t('embedGuardMarketingTitle', 'Content blocked until you allow Marketing & Embeds'),
722        body: t(
723          'embedGuardMarketingBody',
724          'This embedded content may load Marketing and Embeds & Widgets cookies. Enable both to continue.'
725        ),
726        enable: t('embedGuardEnableVideo', 'Enable Marketing & Embeds & continue'),
727        categories: ensureEmbedConsentCategories(cats.length ? cats : ['marketing']),
728      };
729    }
730    if (kind === 'calendly') {
731      return {
732        title: t('embedGuardCalendlyTitle', 'Scheduling blocked until you allow Marketing & Embeds'),
733        body: t(
734          'embedGuardCalendlyBody',
735          'This scheduling embed (Calendly) loads third-party content we cannot fully control. Enable Marketing and Embeds & Widgets to continue.'
736        ),
737        enable: t('embedGuardEnableVideo', 'Enable Marketing & Embeds & continue'),
738        categories: ensureEmbedConsentCategories(cats),
739      };
740    }
741    if (kind === 'widget' || (kind === 'embed' && category === 'functional')) {
742      return {
743        title: t('embedGuardWidgetTitle', 'Form / widget blocked until you allow Marketing & Embeds'),
744        body: t(
745          'embedGuardWidgetBody',
746          'This embedded form or widget loads third-party content we cannot fully control. Enable Marketing and Embeds & Widgets to continue.'
747        ),
748        enable: t('embedGuardEnableVideo', 'Enable Marketing & Embeds & continue'),
749        categories: ensureEmbedConsentCategories(cats),
750      };
751    }
752    return {
753      title: t('embedGuardGenericTitle', 'Content blocked until you allow the required cookies').replace('%s', label),
754      body: t(
755        'embedGuardGenericBody',
756        'This content needs additional cookies before it can load. Enable the required category to continue.'
757      ),
758      enable: (t('embedGuardEnableCategory', 'Enable %s & continue') || 'Enable %s & continue').replace('%s', label),
759      categories: cats.length ? cats : [category],
760    };
761  }
762
763  /**
764   * Third-party iframes/embeds can load Marketing trackers we cannot inspect —
765   * always unlock Marketing + Embeds together (same rule as video players).
766   */
767  function ensureEmbedConsentCategories(categories) {
768    var out = [];
769    var seen = {};
770    function add(slug) {
771      if (!slug || seen[slug]) {
772        return;
773      }
774      seen[slug] = true;
775      out.push(slug);
776    }
777    if (Array.isArray(categories)) {
778      categories.forEach(add);
779    }
780    add('marketing');
781    add('functional');
782    return out;
783  }
784
785  /** @deprecated Use ensureEmbedConsentCategories — kept for older call sites. */
786  function ensureVideoConsentCategories(categories) {
787    return ensureEmbedConsentCategories(categories);
788  }
789
790  function onEnableCategories(categories, e) {
791    if (e) {
792      e.preventDefault();
793      e.stopPropagation();
794    }
795    var list = Array.isArray(categories) ? categories.filter(Boolean) : [categories];
796    if (!list.length) {
797      return;
798    }
799    // Pair Marketing ↔ Embeds for embed unlocks. Pure Security (CAPTCHA) stays alone.
800    var securityOnly = list.indexOf('security') !== -1 && list.indexOf('marketing') === -1 && list.indexOf('analytics') === -1 && list.indexOf('functional') === -1;
801    if (!securityOnly && (list.indexOf('marketing') !== -1 || list.indexOf('functional') !== -1)) {
802      if (list.indexOf('marketing') === -1) {
803        list = list.concat(['marketing']);
804      }
805      if (list.indexOf('functional') === -1) {
806        list = list.concat(['functional']);
807      }
808    }
809    if (!window.UCPF || typeof window.UCPF.getConsent !== 'function' || typeof window.UCPF.setConsent !== 'function') {
810      if (window.UCPF && window.UCPF.openPreferences) {
811        window.UCPF.openPreferences({ highlight: list[0] });
812      }
813      return;
814    }
815    var current = window.UCPF.getConsent() || {};
816    var cats = Object.assign({}, current.categories || {});
817    cats.necessary = true;
818    list.forEach(function (slug) {
819      cats[slug] = true;
820    });
821    window.UCPF.setConsent({
822      state: 'custom',
823      categories: cats,
824      services: current.services || {},
825      uuid: current.uuid || '',
826    });
827  }
828
829  function onEnableCategory(category, e) {
830    onEnableCategories([category], e);
831  }
832
833  function hasAllCategories(categories) {
834    var list = Array.isArray(categories) ? categories : categories ? [categories] : [];
835    if (!list.length) {
836      return true;
837    }
838    for (var i = 0; i < list.length; i++) {
839      if (!hasCategoryConsent(list[i])) {
840        return false;
841      }
842    }
843    return true;
844  }
845
846  function isCheckoutSurface(node) {
847    if (!node) {
848      return false;
849    }
850    if (node.matches && node.matches(CHECKOUT_SELECTORS.join(','))) {
851      return true;
852    }
853    if (node.closest) {
854      for (var i = 0; i < CHECKOUT_SELECTORS.length; i++) {
855        try {
856          if (node.closest(CHECKOUT_SELECTORS[i])) {
857            return true;
858          }
859        } catch (eClose) {
860          /* ignore invalid selector */
861        }
862      }
863    }
864    return false;
865  }
866
867  function onOpenPrefs(category, e) {
868    if (e) {
869      e.preventDefault();
870      e.stopPropagation();
871    }
872    if (window.UCPF && typeof window.UCPF.openPreferences === 'function') {
873      window.UCPF.openPreferences({ highlight: category });
874    }
875  }
876
877  function buildPanel(kind, category, categories) {
878    var copy = copyForKind(kind, category, categories);
879    var enableCats = copy.categories && copy.categories.length ? copy.categories : [category];
880    var panel = document.createElement('div');
881    panel.className = 'ucpf-consent-guard__panel';
882    panel.setAttribute('role', 'alert');
883    panel.setAttribute('aria-live', 'assertive');
884
885    var title = document.createElement('p');
886    title.className = 'ucpf-consent-guard__title';
887    title.textContent = copy.title;
888
889    var body = document.createElement('p');
890    body.className = 'ucpf-consent-guard__body';
891    body.textContent = copy.body;
892
893    var actions = document.createElement('div');
894    actions.className = 'ucpf-consent-guard__actions';
895
896    var enableBtn = document.createElement('button');
897    enableBtn.type = 'button';
898    enableBtn.className = 'ucpf-consent-guard__btn ucpf-consent-guard__btn--primary';
899    enableBtn.setAttribute('data-ucpf-captcha-skip', '1');
900    enableBtn.textContent = copy.enable;
901    enableBtn.addEventListener(
902      'click',
903      function (ev) {
904        // Third-party iframes/embeds cannot be inspected — always grant Marketing + Embeds.
905        var dualEmbed =
906          kind === 'youtube' ||
907          kind === 'vimeo' ||
908          kind === 'widget' ||
909          kind === 'calendly' ||
910          kind === 'map' ||
911          kind === 'embed';
912        var catsToEnable = dualEmbed ? ensureEmbedConsentCategories(enableCats) : enableCats;
913        onEnableCategories(catsToEnable, ev);
914      },
915      true
916    );
917
918    var prefsBtn = document.createElement('button');
919    prefsBtn.type = 'button';
920    prefsBtn.className = 'ucpf-consent-guard__btn ucpf-consent-guard__btn--ghost';
921    prefsBtn.setAttribute('data-ucpf-captcha-skip', '1');
922    prefsBtn.textContent = t('captchaGuardPrefs', 'Cookie Settings');
923    prefsBtn.addEventListener('click', function (ev) {
924      onOpenPrefs(enableCats[0] || category, ev);
925    });
926
927    actions.appendChild(enableBtn);
928    actions.appendChild(prefsBtn);
929    panel.appendChild(title);
930    panel.appendChild(body);
931    panel.appendChild(actions);
932    return panel;
933  }
934
935  function isFormShell(node) {
936    if (!node) {
937      return false;
938    }
939    if (node.tagName === 'FORM') {
940      return true;
941    }
942    if (!node.classList) {
943      return false;
944    }
945    return (
946      node.classList.contains('woocommerce-checkout') ||
947      node.classList.contains('wc-block-checkout') ||
948      node.classList.contains('wp-block-woocommerce-checkout') ||
949      node.classList.contains('forminator-custom-form') ||
950      node.classList.contains('forminator-ui') ||
951      node.classList.contains('wpforms-form') ||
952      node.classList.contains('wpcf7-form') ||
953      node.classList.contains('elementor-form') ||
954      node.classList.contains('frm-show-form') ||
955      node.classList.contains('formidable') ||
956      node.classList.contains('quform-form') ||
957      node.classList.contains('happyforms-form') ||
958      node.classList.contains('jet-form-builder') ||
959      node.classList.contains('kb-form') ||
960      node.classList.contains('et_pb_contact_form') ||
961      node.id === 'customer_details' ||
962      node.id === 'order_review'
963    );
964  }
965
966  /**
967   * Never decorate / wrap document chrome — wrapping <body> nukes Elementor/GF/sticky.
968   * @param {Element|null} node
969   * @return {boolean}
970   */
971  function isForbiddenGuardHost(node) {
972    if (!node || node.nodeType !== 1) {
973      return true;
974    }
975    var tag = String(node.tagName || '').toUpperCase();
976    if (tag === 'HTML' || tag === 'HEAD' || tag === 'BODY') {
977      return true;
978    }
979    if (node.id === 'ucpf-root') {
980      return true;
981    }
982    return false;
983  }
984
985  /** Accessibility toolbar CDN / hosts — never park or cover. */
986  function isUserWayUrl(url) {
987    var u = String(url || '').toLowerCase();
988    if (!u) {
989      return false;
990    }
991    return (
992      u.indexOf('cdn.userway.org') !== -1 ||
993      u.indexOf('api.userway.org') !== -1 ||
994      u.indexOf('userway.org') !== -1
995    );
996  }
997
998  /**
999   * UserWay DOM / scripts — fully hands-off (ADA toolbar).
1000   * @param {Element|null} node
1001   * @return {boolean}
1002   */
1003  function isUserWayNode(node) {
1004    if (!node || node.nodeType !== 1) {
1005      return false;
1006    }
1007    try {
1008      var id = String(node.id || '').toLowerCase();
1009      if (id.indexOf('userway') !== -1 || id === 'userwayaccessibilityicon') {
1010        return true;
1011      }
1012      var cls = String(node.className || '').toLowerCase();
1013      if (cls.indexOf('userway') !== -1 || cls.indexOf('uwy') !== -1) {
1014        return true;
1015      }
1016      var blob = (
1017        (node.getAttribute('data-src') || '') +
1018        ' ' +
1019        (node.getAttribute('src') || '') +
1020        ' ' +
1021        (node.getAttribute('data-account') || '') +
1022        ' ' +
1023        (node.getAttribute('href') || '')
1024      ).toLowerCase();
1025      if (isUserWayUrl(blob)) {
1026        return true;
1027      }
1028      if (node.closest) {
1029        if (
1030          node.closest('#userwayAccessibilityIcon') ||
1031          node.closest('[id*="userway" i]') ||
1032          node.closest('[class*="userway" i]') ||
1033          node.closest('.userway_buttons_wrapper') ||
1034          node.closest('.uwy')
1035        ) {
1036          return true;
1037        }
1038      }
1039    } catch (eUw) {
1040      /* :i selector unsupported — fall through */
1041      try {
1042        if (node.closest && (node.closest('.userway_buttons_wrapper') || node.closest('#userwayAccessibilityIcon'))) {
1043          return true;
1044        }
1045      } catch (e2) { /* ignore */ }
1046    }
1047    return false;
1048  }
1049
1050  function isBackgroundVideoOwner(node) {
1051    if (!node || !node.getAttribute) {
1052      return false;
1053    }
1054    var settings = node.getAttribute('data-settings') || '';
1055    return settings.indexOf('background_video_link') !== -1;
1056  }
1057
1058  function isBackgroundVideoShell(node) {
1059    if (!node || !node.classList) {
1060      return false;
1061    }
1062    if (isBackgroundVideoOwner(node)) {
1063      return true;
1064    }
1065    return (
1066      node.classList.contains('elementor-background-video-container') ||
1067      node.classList.contains('elementor-background-video-embed')
1068    );
1069  }
1070
1071  /**
1072   * Prefer the Elementor e-con that owns background_video_link.
1073   * Decorating .elementor-background-video-container fights Elementor's absolute fill.
1074   *
1075   * @param {Element} node
1076   * @return {Element|null}
1077   */
1078  function resolveBackgroundVideoHost(node) {
1079    if (!node) {
1080      return null;
1081    }
1082    if (isBackgroundVideoOwner(node)) {
1083      return node;
1084    }
1085    if (node.closest) {
1086      var owner = node.closest('[data-settings*="background_video_link"]');
1087      if (owner) {
1088        return owner;
1089      }
1090    }
1091    if (node.classList.contains('elementor-background-video-embed')) {
1092      var box = node.closest ? node.closest('.elementor-background-video-container') : node.parentElement;
1093      return resolveBackgroundVideoHost(box || node);
1094    }
1095    if (node.classList.contains('elementor-background-video-container') && node.parentElement) {
1096      return node.parentElement;
1097    }
1098    return node;
1099  }
1100
1101  /**
1102   * Smash Balloon YouTube Feed / Social Wall roots — decorate in place.
1103   * Never treat #youtube-feed-loader (spinner) as a cover host.
1104   *
1105   * @param {Element|null} node
1106   * @return {boolean}
1107   */
1108  function isYoutubeFeedLoader(node) {
1109    if (!node || node.nodeType !== 1) {
1110      return false;
1111    }
1112    if (node.id === 'youtube-feed-loader') {
1113      return true;
1114    }
1115    try {
1116      return !!(node.closest && node.closest('#youtube-feed-loader'));
1117    } catch (eLoad) {
1118      return false;
1119    }
1120  }
1121
1122  function isSmashBalloonFeedHost(node) {
1123    if (!node || node.nodeType !== 1) {
1124      return false;
1125    }
1126    if (isYoutubeFeedLoader(node)) {
1127      return false;
1128    }
1129    var id = String(node.id || '');
1130    if (id === 'regular-youtube-container' || id === 'live-youtube-container') {
1131      return true;
1132    }
1133    if (id.indexOf('sb_youtube') === 0 || id.indexOf('sb-wall') === 0) {
1134      return true;
1135    }
1136    if (!node.classList) {
1137      return false;
1138    }
1139    return node.classList.contains('sb_youtube') || node.classList.contains('sb-wall');
1140  }
1141
1142  function youtubeFeedHostContainer(node) {
1143    if (!node || isYoutubeFeedLoader(node)) {
1144      return null;
1145    }
1146    if (node.classList && node.classList.contains('sb_youtube')) {
1147      return node;
1148    }
1149    if (node.closest) {
1150      var feed = node.closest('.sb_youtube');
1151      if (feed && !isYoutubeFeedLoader(feed)) {
1152        return feed;
1153      }
1154    }
1155    if (node.querySelector) {
1156      var inner = node.querySelector('.sb_youtube');
1157      if (inner && !isYoutubeFeedLoader(inner)) {
1158        return inner;
1159      }
1160    }
1161    if (node.id === 'regular-youtube-container' || node.id === 'live-youtube-container') {
1162      return node;
1163    }
1164    return isSmashBalloonFeedHost(node) ? node : null;
1165  }
1166
1167  function socialWallHostContainer(node) {
1168    if (!node || node.nodeType !== 1) {
1169      return null;
1170    }
1171    if (node.classList && node.classList.contains('sb-wall')) {
1172      return node;
1173    }
1174    if (node.closest) {
1175      var wall = node.closest('.sb-wall');
1176      if (wall) {
1177        return wall;
1178      }
1179    }
1180    if (node.querySelector) {
1181      var innerWall = node.querySelector('.sb-wall');
1182      if (innerWall) {
1183        return innerWall;
1184      }
1185    }
1186    var id = String(node.id || '');
1187    if (id.indexOf('sb-wall') === 0) {
1188      return node;
1189    }
1190    return null;
1191  }
1192
1193  function isBuilderShell(node) {
1194    if (!node || !node.classList) {
1195      return false;
1196    }
1197    if (node.getAttribute('data-widget_type') === 'video.default') {
1198      return true;
1199    }
1200    if (node.getAttribute('data-widget_type') === 'html.default') {
1201      return true;
1202    }
1203    if (node.getAttribute('data-widget_type') === 'google_maps.default') {
1204      return true;
1205    }
1206    var wt = node.getAttribute('data-widget_type') || '';
1207    if (wt.indexOf('google_maps') !== -1) {
1208      return true;
1209    }
1210    if (isBackgroundVideoShell(node) || isBackgroundVideoOwner(node)) {
1211      return true;
1212    }
1213    if (isMapShell(node)) {
1214      return true;
1215    }
1216    return (
1217      node.classList.contains('elementor-widget-video') ||
1218      node.classList.contains('elementor-widget-html') ||
1219      node.classList.contains('elementor-widget-google_maps') ||
1220      node.classList.contains('elementor-widget-container') ||
1221      node.classList.contains('et_pb_video') ||
1222      node.classList.contains('wpb_video_widget') ||
1223      node.classList.contains('fl-module-video') ||
1224      node.classList.contains('brxe-video') ||
1225      node.classList.contains('wp-block-embed-youtube') ||
1226      node.classList.contains('wp-block-embed-vimeo') ||
1227      node.classList.contains('wp-block-video') ||
1228      node.classList.contains('wp-block-embed') ||
1229      node.classList.contains('jobber-inline-work-request') ||
1230      isSmashBalloonFeedHost(node)
1231    );
1232  }
1233
1234  /**
1235   * Outer map widget shells — decorate in place (do not wrap / nest covers).
1236   *
1237   * @param {Element} node
1238   * @return {boolean}
1239   */
1240  function isMapShell(node) {
1241    if (!node || !node.classList) {
1242      return false;
1243    }
1244    if (node.classList.contains('elementor-widget-google_maps')) {
1245      return true;
1246    }
1247    if (node.classList.contains('wpgmza_map_container') || node.classList.contains('wpgmza_map')) {
1248      return true;
1249    }
1250    if (
1251      node.classList.contains('mapster-wp-maps-container') ||
1252      node.classList.contains('mapster-wp-maps') ||
1253      node.classList.contains('mapster-map')
1254    ) {
1255      return true;
1256    }
1257    var id = String(node.id || '');
1258    if (id.indexOf('mapster-wp-maps') === 0) {
1259      return true;
1260    }
1261    return false;
1262  }
1263
1264  /**
1265   * Prefer the outermost map host so nested markers (gm-style inside wpgmza, etc.)
1266   * do not each get their own consent cover.
1267   *
1268   * @param {Element} node
1269   * @return {Element|null}
1270   */
1271  function resolveMapHost(node) {
1272    if (!node || node.nodeType !== 1) {
1273      return null;
1274    }
1275    if (node.closest) {
1276      var elWidget =
1277        node.closest('.elementor-widget-google_maps') ||
1278        node.closest('[data-widget_type="google_maps.default"]') ||
1279        node.closest('[data-widget_type*="google_maps"]');
1280      if (elWidget) {
1281        return elWidget;
1282      }
1283      var wpgmzaOuter = node.closest('.wpgmza_map_container');
1284      if (wpgmzaOuter) {
1285        return wpgmzaOuter;
1286      }
1287      var mapsterOuter =
1288        node.closest('.mapster-wp-maps-container') ||
1289        node.closest('.mapster-wp-maps') ||
1290        node.closest('[id^="mapster-wp-maps"]');
1291      if (mapsterOuter) {
1292        return mapsterOuter;
1293      }
1294      var leaflet = node.closest('.leaflet-container');
1295      if (leaflet) {
1296        return leaflet;
1297      }
1298      var mapbox = node.closest('.mapboxgl-map');
1299      if (mapbox) {
1300        return mapbox;
1301      }
1302      var maplibre = node.closest('.maplibregl-map');
1303      if (maplibre && !node.closest('.mapster-wp-maps, .mapster-wp-maps-container')) {
1304        return maplibre;
1305      }
1306    }
1307    // Google's internal .gm-style is never a good cover host by itself.
1308    if (node.classList && node.classList.contains('gm-style')) {
1309      if (node.parentElement) {
1310        var parentMap = resolveMapHost(node.parentElement);
1311        if (parentMap && parentMap !== node) {
1312          return parentMap;
1313        }
1314        return node.parentElement;
1315      }
1316    }
1317    if (node.tagName === 'IFRAME' && node.parentElement) {
1318      if (node.parentElement.classList && node.parentElement.classList.contains('ucpf-consent-guard')) {
1319        return node;
1320      }
1321      var iframeHost = resolveMapHost(node.parentElement);
1322      return iframeHost || node.parentElement;
1323    }
1324    return node;
1325  }
1326
1327  /**
1328   * Elementor (and similar) video widgets that gate before an iframe exists.
1329   * Empty .elementor-video shells measure ~0px — absolute overlays then vanish.
1330   *
1331   * @param {Element} host
1332   * @return {boolean}
1333   */
1334  function isCollapsedVideoShell(host) {
1335    if (!host || !host.classList) {
1336      return false;
1337    }
1338    if (
1339      host.classList.contains('elementor-widget-video') ||
1340      host.getAttribute('data-widget_type') === 'video.default' ||
1341      host.classList.contains('et_pb_video') ||
1342      host.classList.contains('wpb_video_widget') ||
1343      host.classList.contains('fl-module-video') ||
1344      host.classList.contains('brxe-video') ||
1345      host.classList.contains('wp-block-embed-youtube') ||
1346      host.classList.contains('wp-block-embed-vimeo')
1347    ) {
1348      return true;
1349    }
1350    var kind = host.getAttribute('data-ucpf-guard-kind') || '';
1351    return kind === 'youtube' || kind === 'vimeo';
1352  }
1353
1354  /**
1355   * Map widgets that often measure ~0px before tiles paint.
1356   *
1357   * @param {Element} host
1358   * @return {boolean}
1359   */
1360  function isCollapsedMapShell(host) {
1361    if (!host || host.nodeType !== 1) {
1362      return false;
1363    }
1364    if (isMapShell(host)) {
1365      return true;
1366    }
1367    var kind = host.getAttribute('data-ucpf-guard-kind') || '';
1368    if (kind === 'map') {
1369      return true;
1370    }
1371    if (!host.classList) {
1372      return false;
1373    }
1374    return (
1375      host.classList.contains('elementor-widget-google_maps') ||
1376      host.classList.contains('wpgmza_map_container') ||
1377      host.classList.contains('wpgmza_map') ||
1378      host.classList.contains('mapster-wp-maps-container') ||
1379      host.classList.contains('mapster-wp-maps') ||
1380      host.classList.contains('maplibregl-map') ||
1381      host.classList.contains('mapboxgl-map') ||
1382      host.classList.contains('leaflet-container')
1383    );
1384  }
1385
1386  /**
1387   * True for embed/widget consent hosts (not forms, not background video).
1388   * Used for the generic collapsed-shell floor (Calendly, HTML widgets, etc.).
1389   *
1390   * @param {Element} host
1391   * @return {boolean}
1392   */
1393  function isEmbedGuardHost(host) {
1394    if (!host || host.nodeType !== 1) {
1395      return false;
1396    }
1397    if (host.classList) {
1398      if (host.classList.contains('ucpf-consent-guard--form')) {
1399        return false;
1400      }
1401      if (host.classList.contains('ucpf-consent-guard--bg')) {
1402        return false;
1403      }
1404      if (host.classList.contains('ucpf-consent-guard--embed')) {
1405        return true;
1406      }
1407    }
1408    var kind = host.getAttribute('data-ucpf-guard-kind') || '';
1409    return (
1410      kind === 'calendly' ||
1411      kind === 'widget' ||
1412      kind === 'embed' ||
1413      kind === 'youtube' ||
1414      kind === 'vimeo' ||
1415      kind === 'map'
1416    );
1417  }
1418
1419  /**
1420   * Apply a modest min-height so absolute inset panels have a paintable box
1421   * when the host is still collapsed (hidden popup, empty widget shell, etc.).
1422   *
1423   * @param {Element} host
1424   * @return {void}
1425   */
1426  function applyEmbedFloor(host) {
1427    if (!host || host.nodeType !== 1) {
1428      return;
1429    }
1430    try {
1431      host.style.setProperty('min-height', '22rem', 'important');
1432      host.style.setProperty('width', '100%', 'important');
1433      host.setAttribute('data-ucpf-size-locked', '1');
1434      host.setAttribute('data-ucpf-guard-min-h', 'embed-floor');
1435      host.setAttribute('data-ucpf-aspect-fallback', '1');
1436      var floorInner =
1437        host.querySelector('.calendly-inline-widget') ||
1438        host.querySelector('[data-url*="calendly"]') ||
1439        host.querySelector('iframe[data-ucpf-parked="1"]') ||
1440        host.querySelector('iframe[data-src]') ||
1441        host.querySelector('.elementor-widget-container');
1442      if (floorInner && floorInner !== host && !floorInner.getAttribute('data-ucpf-aspect-fallback')) {
1443        floorInner.style.setProperty('min-height', '22rem', 'important');
1444        floorInner.style.setProperty('width', '100%', 'important');
1445        floorInner.setAttribute('data-ucpf-aspect-fallback', '1');
1446      }
1447    } catch (eFloor) { /* ignore */ }
1448  }
1449
1450  /**
1451   * Lock the host to its pre-gate size so parked iframes don’t collapse the box.
1452   * Video/map get aspect fallbacks; other embeds get a generic floor when collapsed
1453   * (Elementor popups often measure 0 until shown). Forms never invent height.
1454   *
1455   * @param {Element} host
1456   * @return {void}
1457   */
1458  function preserveEmbedBoxSize(host) {
1459    if (!host || host.nodeType !== 1) {
1460      return;
1461    }
1462    if (host.getAttribute('data-ucpf-size-locked') === '1') {
1463      return;
1464    }
1465    // Background video e-cons: Elementor owns geometry — never invent min-height.
1466    if (isBackgroundVideoOwner(host) || (host.classList && host.classList.contains('ucpf-consent-guard--bg'))) {
1467      return;
1468    }
1469
1470    var measured = 0;
1471    try {
1472      measured = Math.round(host.getBoundingClientRect().height || 0);
1473    } catch (eRect) {
1474      measured = 0;
1475    }
1476
1477    var attrH = 0;
1478    var iframes = [];
1479    try {
1480      if (host.tagName === 'IFRAME') {
1481        iframes = [host];
1482      } else if (host.querySelectorAll) {
1483        iframes = Array.prototype.slice.call(host.querySelectorAll('iframe'));
1484      }
1485    } catch (eIf) {
1486      iframes = [];
1487    }
1488    for (var i = 0; i < iframes.length; i++) {
1489      var iframe = iframes[i];
1490      var hAttr = parseInt(iframe.getAttribute('height'), 10) || 0;
1491      var styleH = 0;
1492      try {
1493        var st = iframe.style && iframe.style.height ? parseInt(iframe.style.height, 10) : 0;
1494        styleH = st || 0;
1495      } catch (eSt) {
1496        styleH = 0;
1497      }
1498      var rectH = 0;
1499      try {
1500        rectH = Math.round(iframe.getBoundingClientRect().height || 0);
1501      } catch (eRh) {
1502        rectH = 0;
1503      }
1504      attrH = Math.max(attrH, hAttr, styleH, rectH);
1505      // Keep the iframe itself from collapsing when src is parked.
1506      if (Math.max(hAttr, styleH, rectH) >= 40 && !iframe.getAttribute('data-ucpf-iframe-h')) {
1507        var keep = Math.max(hAttr, styleH, rectH);
1508        iframe.setAttribute('data-ucpf-iframe-h', String(keep));
1509        try {
1510          iframe.style.setProperty('min-height', keep + 'px', 'important');
1511          iframe.style.setProperty('height', keep + 'px', 'important');
1512        } catch (eKeep) { /* ignore */ }
1513      }
1514    }
1515
1516    var targetH = Math.max(measured, attrH);
1517    // Empty Elementor YouTube/Vimeo shells: invent a 16:9 box so the glass overlay is visible.
1518    if (targetH < 80 && isCollapsedVideoShell(host)) {
1519      try {
1520        host.style.setProperty('aspect-ratio', '16 / 9', 'important');
1521        host.style.setProperty('width', '100%', 'important');
1522        host.style.setProperty('min-height', '12rem', 'important');
1523        host.setAttribute('data-ucpf-size-locked', '1');
1524        host.setAttribute('data-ucpf-guard-min-h', 'aspect-16-9');
1525        host.setAttribute('data-ucpf-aspect-fallback', '1');
1526        // Give the inner Elementor fill a height so layout isn’t empty under the panel.
1527        var inner =
1528          host.querySelector('.elementor-wrapper') ||
1529          host.querySelector('.elementor-video') ||
1530          host.querySelector('.elementor-widget-container');
1531        if (inner && !inner.getAttribute('data-ucpf-aspect-fallback')) {
1532          inner.style.setProperty('aspect-ratio', '16 / 9', 'important');
1533          inner.style.setProperty('min-height', '12rem', 'important');
1534          inner.style.setProperty('width', '100%', 'important');
1535          inner.setAttribute('data-ucpf-aspect-fallback', '1');
1536        }
1537      } catch (eAspect) { /* ignore */ }
1538      return;
1539    }
1540    // Collapsed map shells: modest 16:9 so Marketing+Embeds cover is visible before tiles.
1541    if (targetH < 80 && isCollapsedMapShell(host)) {
1542      try {
1543        host.style.setProperty('aspect-ratio', '16 / 9', 'important');
1544        host.style.setProperty('width', '100%', 'important');
1545        host.style.setProperty('min-height', '14rem', 'important');
1546        host.setAttribute('data-ucpf-size-locked', '1');
1547        host.setAttribute('data-ucpf-guard-min-h', 'aspect-16-9-map');
1548        host.setAttribute('data-ucpf-aspect-fallback', '1');
1549        var mapInner =
1550          host.querySelector('.elementor-widget-container') ||
1551          host.querySelector('.wpgmza_map') ||
1552          host.querySelector('.mapster-wp-maps') ||
1553          host.querySelector('.maplibregl-map');
1554        if (mapInner && mapInner !== host && !mapInner.getAttribute('data-ucpf-aspect-fallback')) {
1555          mapInner.style.setProperty('aspect-ratio', '16 / 9', 'important');
1556          mapInner.style.setProperty('min-height', '14rem', 'important');
1557          mapInner.style.setProperty('width', '100%', 'important');
1558          mapInner.setAttribute('data-ucpf-aspect-fallback', '1');
1559        }
1560      } catch (eMapAspect) { /* ignore */ }
1561      return;
1562    }
1563    // Collapsed Calendly / HTML widgets / other embeds (e.g. Elementor popup not yet shown).
1564    if (targetH < 80) {
1565      if (isEmbedGuardHost(host)) {
1566        applyEmbedFloor(host);
1567      }
1568      return;
1569    }
1570    // Cap runaway measurements (full-page wrappers).
1571    if (targetH > 1200) {
1572      targetH = Math.min(targetH, Math.max(measured, attrH, 480));
1573    }
1574    try {
1575      host.style.setProperty('min-height', targetH + 'px', 'important');
1576      host.setAttribute('data-ucpf-size-locked', '1');
1577      host.setAttribute('data-ucpf-guard-min-h', String(targetH));
1578    } catch (eLock) { /* ignore */ }
1579  }
1580
1581  /**
1582   * Clear an embed-floor lock and re-measure (e.g. after Elementor popup opens).
1583   *
1584   * @param {Element} host
1585   * @return {void}
1586   */
1587  function refreshEmbedBoxSize(host) {
1588    if (!host || host.nodeType !== 1) {
1589      return;
1590    }
1591    if (host.getAttribute('data-ucpf-guard-min-h') === 'embed-floor') {
1592      clearEmbedBoxSize(host);
1593    }
1594    preserveEmbedBoxSize(host);
1595  }
1596
1597  /**
1598   * Re-run size lock for active embed guards under a root (popup body, document).
1599   *
1600   * @param {Element|Document|null} root
1601   * @return {void}
1602   */
1603  function remeasureActiveEmbedGuards(root) {
1604    var scope = root && root.querySelectorAll ? root : document;
1605    var hosts;
1606    try {
1607      hosts = scope.querySelectorAll('.ucpf-consent-guard--embed.ucpf-consent-guard--active');
1608    } catch (eQ) {
1609      return;
1610    }
1611    Array.prototype.forEach.call(hosts, function (host) {
1612      refreshEmbedBoxSize(host);
1613    });
1614  }
1615
1616  /**
1617   * Elementor popups often attach guards while display:none — remasure on show.
1618   *
1619   * @return {void}
1620   */
1621  function bindElementorPopupRemeasure() {
1622    function resolvePopupRoot(id, instance) {
1623      var root = null;
1624      try {
1625        if (instance && instance.$element && instance.$element[0]) {
1626          root = instance.$element[0];
1627        } else if (instance && instance.$ && instance.$[0]) {
1628          root = instance.$[0];
1629        }
1630      } catch (eInst) {
1631        root = null;
1632      }
1633      if (!root && id != null && id !== '') {
1634        try {
1635          root =
1636            document.getElementById('elementor-popup-modal-' + id) ||
1637            document.querySelector('[data-elementor-id="' + id + '"].elementor-location-popup') ||
1638            document.querySelector('.elementor-popup-modal[data-elementor-id="' + id + '"]');
1639        } catch (eId) {
1640          root = null;
1641        }
1642      }
1643      if (!root) {
1644        try {
1645          root =
1646            document.querySelector('.elementor-popup-modal:not([aria-hidden="true"]) .dialog-lightbox-message') ||
1647            document.querySelector('.elementor-popup-modal .dialog-widget-content') ||
1648            document.querySelector('.elementor-location-popup');
1649        } catch (eFb) {
1650          root = null;
1651        }
1652      }
1653      return root;
1654    }
1655
1656    function onPopupShow(event, id, instance) {
1657      var root = resolvePopupRoot(id, instance);
1658      var run = function () {
1659        remeasureActiveEmbedGuards(root || document);
1660      };
1661      window.setTimeout(run, 50);
1662      window.setTimeout(run, 300);
1663    }
1664
1665    try {
1666      document.addEventListener('elementor/popup/show', function (ev) {
1667        var detail = ev && ev.detail ? ev.detail : null;
1668        var id = detail && detail.id != null ? detail.id : null;
1669        var instance = detail && detail.instance ? detail.instance : null;
1670        onPopupShow(ev, id, instance);
1671      });
1672    } catch (eNat) { /* ignore */ }
1673
1674    function bindJquery() {
1675      try {
1676        if (!window.jQuery || !window.jQuery.fn) {
1677          return false;
1678        }
1679        window.jQuery(document).on('elementor/popup/show', onPopupShow);
1680        return true;
1681      } catch (eJq) {
1682        return false;
1683      }
1684    }
1685    if (!bindJquery()) {
1686      [100, 500, 2000].forEach(function (ms) {
1687        window.setTimeout(bindJquery, ms);
1688      });
1689    }
1690  }
1691
1692  /**
1693   * Light fallback when Elementor's popup event is missed: remasure floor-locked
1694   * embeds inside a visible popup / lightbox message.
1695   *
1696   * @return {void}
1697   */
1698  function remeasureVisiblePopupEmbeds() {
1699    var roots;
1700    try {
1701      roots = document.querySelectorAll(
1702        '.elementor-popup-modal .dialog-lightbox-message, .elementor-location-popup, .dialog-lightbox-message'
1703      );
1704    } catch (eRoots) {
1705      return;
1706    }
1707    Array.prototype.forEach.call(roots, function (root) {
1708      if (!root || !root.querySelector) {
1709        return;
1710      }
1711      var floor = null;
1712      try {
1713        floor = root.querySelector(
1714          '.ucpf-consent-guard--embed.ucpf-consent-guard--active[data-ucpf-guard-min-h="embed-floor"]'
1715        );
1716      } catch (eFloor) {
1717        floor = null;
1718      }
1719      if (!floor) {
1720        return;
1721      }
1722      try {
1723        var cs = window.getComputedStyle ? window.getComputedStyle(root) : null;
1724        if (cs && (cs.display === 'none' || cs.visibility === 'hidden')) {
1725          return;
1726        }
1727      } catch (eCs) { /* continue */ }
1728      remeasureActiveEmbedGuards(root);
1729    });
1730  }
1731
1732  /**
1733   * Undo preserveEmbedBoxSize.
1734   *
1735   * @param {Element} host
1736   * @return {void}
1737   */
1738  function clearEmbedBoxSize(host) {
1739    if (!host || host.nodeType !== 1) {
1740      return;
1741    }
1742    if (host.getAttribute('data-ucpf-size-locked') === '1') {
1743      try {
1744        host.style.removeProperty('min-height');
1745        if (host.getAttribute('data-ucpf-aspect-fallback') === '1') {
1746          host.style.removeProperty('aspect-ratio');
1747          host.style.removeProperty('width');
1748        }
1749      } catch (eRm) { /* ignore */ }
1750      host.removeAttribute('data-ucpf-size-locked');
1751      host.removeAttribute('data-ucpf-guard-min-h');
1752      host.removeAttribute('data-ucpf-aspect-fallback');
1753    }
1754    try {
1755      var inners = host.querySelectorAll
1756        ? host.querySelectorAll('[data-ucpf-aspect-fallback="1"]')
1757        : [];
1758      Array.prototype.forEach.call(inners, function (inner) {
1759        if (inner === host) {
1760          return;
1761        }
1762        try {
1763          inner.style.removeProperty('aspect-ratio');
1764          inner.style.removeProperty('min-height');
1765          inner.style.removeProperty('width');
1766        } catch (eIn) { /* ignore */ }
1767        inner.removeAttribute('data-ucpf-aspect-fallback');
1768      });
1769    } catch (eInner) { /* ignore */ }
1770    try {
1771      var nodes = host.tagName === 'IFRAME' ? [host] : host.querySelectorAll ? host.querySelectorAll('iframe[data-ucpf-iframe-h]') : [];
1772      Array.prototype.forEach.call(nodes, function (iframe) {
1773        try {
1774          iframe.style.removeProperty('min-height');
1775          iframe.style.removeProperty('height');
1776        } catch (eI) { /* ignore */ }
1777        iframe.removeAttribute('data-ucpf-iframe-h');
1778      });
1779    } catch (eQ) { /* ignore */ }
1780  }
1781
1782  function isEffectivelyHidden(el) {
1783    if (!el) {
1784      return true;
1785    }
1786    // Builder entrance animations (e.g. Elementor .elementor-invisible) use
1787    // visibility:hidden until Motion FX runs. Consent covers must still attach
1788    // before the embed loads — otherwise the section reveals with no overlay.
1789    var entranceAnim = false;
1790    try {
1791      entranceAnim = !!(el.classList && el.classList.contains('elementor-invisible'));
1792    } catch (eInv) { /* ignore */ }
1793    try {
1794      if (window.getComputedStyle) {
1795        var cs = window.getComputedStyle(el);
1796        if (cs.display === 'none') {
1797          return true;
1798        }
1799        if (cs.visibility === 'hidden' && !entranceAnim) {
1800          return true;
1801        }
1802      }
1803    } catch (e) {
1804      /* ignore */
1805    }
1806    return false;
1807  }
1808
1809  function guardHost(target) {
1810    if (!target || !target.classList) {
1811      return null;
1812    }
1813    if (target.classList.contains('ucpf-consent-guard')) {
1814      return target;
1815    }
1816    var parent = target.parentElement;
1817    if (parent && parent.classList.contains('ucpf-consent-guard')) {
1818      return parent;
1819    }
1820    return null;
1821  }
1822
1823  function ensureWrap(target, mode) {
1824    if (isForbiddenGuardHost(target)) {
1825      return target;
1826    }
1827    var existing = guardHost(target);
1828    if (existing) {
1829      if (isForbiddenGuardHost(existing)) {
1830        return target;
1831      }
1832      return existing;
1833    }
1834
1835    // Elementor background-video e-con: --bg only (never --embed layout rules).
1836    if (mode === 'embed' && isBackgroundVideoOwner(target)) {
1837      target.classList.add('ucpf-consent-guard', 'ucpf-captcha-guard', 'ucpf-consent-guard--bg');
1838      target.setAttribute('data-ucpf-guard-shell', '1');
1839      return target;
1840    }
1841
1842    // Decorate in place — wrapping forms/builder shells breaks theme layout.
1843    if (
1844      (mode === 'form' && isFormShell(target)) ||
1845      (mode === 'embed' && isBuilderShell(target))
1846    ) {
1847      target.classList.add(
1848        'ucpf-consent-guard',
1849        'ucpf-captcha-guard',
1850        mode === 'form' ? 'ucpf-consent-guard--form' : 'ucpf-consent-guard--embed'
1851      );
1852      target.setAttribute('data-ucpf-guard-shell', '1');
1853      return target;
1854    }
1855
1856    // Never insert a wrapper around document chrome (would reparent <body>).
1857    if (!target.parentNode || isForbiddenGuardHost(target.parentNode)) {
1858      return target;
1859    }
1860
1861    var wrap = document.createElement('div');
1862    wrap.className = 'ucpf-consent-guard ucpf-captcha-guard';
1863    wrap.classList.add(mode === 'form' ? 'ucpf-consent-guard--form' : 'ucpf-consent-guard--embed');
1864    wrap.setAttribute('data-ucpf-guard-shell', '0');
1865    if (target.parentNode) {
1866      target.parentNode.insertBefore(wrap, target);
1867      wrap.appendChild(target);
1868    }
1869    return wrap;
1870  }
1871
1872  /** True when URL is a YouTube / Vimeo player embed. */
1873  function isVideoPlayerUrl(url) {
1874    var u = String(url || '').toLowerCase();
1875    if (!u) {
1876      return false;
1877    }
1878    return (
1879      u.indexOf('youtube.com') !== -1 ||
1880      u.indexOf('youtu.be') !== -1 ||
1881      u.indexOf('youtube-nocookie.com') !== -1 ||
1882      u.indexOf('player.vimeo.com') !== -1 ||
1883      u.indexOf('vimeo.com') !== -1
1884    );
1885  }
1886
1887  /**
1888   * Stop players loading under the cover — park src onto data-src until consent.
1889   * Also claim iframes already emptied by network-gate so restore can find them.
1890   * @param {Element} host Guard target (widget / wrapper).
1891   */
1892  function parkVideoIframes(host) {
1893    if (!host || !host.querySelectorAll) {
1894      return;
1895    }
1896    var nodes = host.querySelectorAll('iframe');
1897    Array.prototype.forEach.call(nodes, function (iframe) {
1898      var src = iframe.getAttribute('src') || '';
1899      var parked = iframe.getAttribute('data-src') || '';
1900      var candidate = src && src !== 'about:blank' ? src : parked;
1901      if (!candidate || !isVideoPlayerUrl(candidate)) {
1902        return;
1903      }
1904      if (!iframe.getAttribute('data-src')) {
1905        iframe.setAttribute('data-src', candidate);
1906      }
1907      iframe.setAttribute('data-ucpf-parked', '1');
1908      if (!src || src === 'about:blank') {
1909        return;
1910      }
1911      try {
1912        iframe.removeAttribute('src');
1913      } catch (eRm) { /* ignore */ }
1914      try {
1915        iframe.src = '';
1916      } catch (eSrc) { /* ignore */ }
1917    });
1918  }
1919
1920  /**
1921   * Put a video URL back on an iframe after consent.
1922   * Clears gate stamps, assigns src, and replaces the node if the gate re-blocks
1923   * (YouTube often stays blank on a previously emptied iframe).
1924   *
1925   * @param {HTMLIFrameElement} iframe
1926   * @param {string} src
1927   * @return {boolean}
1928   */
1929  function forceVideoIframeSrc(iframe, src) {
1930    if (!iframe || !src || !isVideoPlayerUrl(src)) {
1931      return false;
1932    }
1933    iframe.removeAttribute('data-ucpf-parked');
1934    iframe.removeAttribute('data-ucpf-gated');
1935    iframe.removeAttribute('data-ucpf-category');
1936    try {
1937      iframe.src = src;
1938    } catch (eProp) {
1939      try {
1940        iframe.setAttribute('src', src);
1941      } catch (eAttr) {
1942        return false;
1943      }
1944    }
1945    iframe.removeAttribute('data-src');
1946    var live = '';
1947    try {
1948      live = iframe.getAttribute('src') || iframe.src || '';
1949    } catch (eLive) {
1950      live = '';
1951    }
1952    if (live && live !== 'about:blank' && isVideoPlayerUrl(live)) {
1953      return true;
1954    }
1955    // Setter still blocked or player dead — swap in a fresh iframe.
1956    if (!iframe.parentNode) {
1957      return false;
1958    }
1959    var fresh = document.createElement('iframe');
1960    Array.prototype.slice.call(iframe.attributes || []).forEach(function (attr) {
1961      if (!attr || !attr.name) {
1962        return;
1963      }
1964      if (
1965        attr.name === 'src' ||
1966        attr.name === 'data-src' ||
1967        attr.name === 'data-ucpf-gated' ||
1968        attr.name === 'data-ucpf-parked' ||
1969        attr.name === 'data-ucpf-category'
1970      ) {
1971        return;
1972      }
1973      try {
1974        fresh.setAttribute(attr.name, attr.value);
1975      } catch (eCopy) { /* ignore */ }
1976    });
1977    if (!fresh.className && iframe.className) {
1978      fresh.className = iframe.className;
1979    }
1980    if (iframe.classList && iframe.classList.contains('elementor-video')) {
1981      fresh.classList.add('elementor-video');
1982    }
1983    fresh.setAttribute('src', src);
1984    try {
1985      iframe.parentNode.replaceChild(fresh, iframe);
1986    } catch (eRep) {
1987      return false;
1988    }
1989    return true;
1990  }
1991
1992  /**
1993   * Restore parked / gated YouTube / Vimeo iframes after consent.
1994   * Covers both guard parking (data-ucpf-parked) and network-gate (data-ucpf-gated).
1995   * @param {Element} host Guard target (or document-wide root).
1996   */
1997  function restoreParkedVideoIframes(host) {
1998    if (!host || !host.querySelectorAll) {
1999      return;
2000    }
2001    var roots = [host];
2002    var wrap = guardHost(host);
2003    if (wrap && wrap !== host) {
2004      roots.push(wrap);
2005    }
2006    roots.forEach(function (root) {
2007      if (!root || !root.querySelectorAll) {
2008        return;
2009      }
2010      Array.prototype.forEach.call(
2011        root.querySelectorAll(
2012          'iframe[data-ucpf-parked="1"], iframe[data-ucpf-gated="1"][data-src], iframe.elementor-video[data-src], iframe[data-src*="youtube"], iframe[data-src*="youtu.be"], iframe[data-src*="youtube-nocookie"], iframe[data-src*="vimeo"]'
2013        ),
2014        function (iframe) {
2015          var live = iframe.getAttribute('src') || '';
2016          if (live && live !== 'about:blank' && isVideoPlayerUrl(live)) {
2017            iframe.removeAttribute('data-ucpf-parked');
2018            iframe.removeAttribute('data-ucpf-gated');
2019            return;
2020          }
2021          var real = iframe.getAttribute('data-src') || '';
2022          if (!real || !isVideoPlayerUrl(real)) {
2023            iframe.removeAttribute('data-ucpf-parked');
2024            return;
2025          }
2026          forceVideoIframeSrc(iframe, real);
2027        }
2028      );
2029    });
2030  }
2031
2032  /** Document-wide video iframe restore (maps-style — not only under a guard host). */
2033  function restoreAllParkedVideoIframes() {
2034    restoreParkedVideoIframes(document);
2035  }
2036
2037  function applyGuard(target, kind, category, mode, categories) {
2038    var cats = Array.isArray(categories) && categories.length ? categories : category ? [category] : [];
2039    if (!target || isForbiddenGuardHost(target) || isUserWayNode(target) || hasAllCategories(cats)) {
2040      return;
2041    }
2042    if (mode === 'embed' && isEffectivelyHidden(target) && !isSmashBalloonFeedHost(target)) {
2043      return;
2044    }
2045    var key = kind + ':' + cats.join('+');
2046    var isVideoKind = kind === 'youtube' || kind === 'vimeo';
2047    if (guarded.get(target) === key && target.getAttribute('data-ucpf-consent-guarded') === '1') {
2048      var existing = guardHost(target) || target;
2049      if (existing && existing.classList.contains('ucpf-consent-guard')) {
2050        syncThemeOnto(existing);
2051      }
2052      // Retry size lock: first pass often measured 0 before Elementor painted the shell.
2053      if (mode === 'embed' && isVideoKind && existing) {
2054        preserveEmbedBoxSize(existing);
2055        if (existing !== target) {
2056          preserveEmbedBoxSize(target);
2057        }
2058      }
2059      // Elementor may re-inject src after we parked — keep players unloaded.
2060      if (isVideoKind || (mode === 'embed' && isBackgroundVideoOwner(target))) {
2061        parkVideoIframes(target);
2062        if (existing && existing !== target) {
2063          parkVideoIframes(existing);
2064        }
2065      }
2066      return;
2067    }
2068
2069    var isBg = mode === 'embed' && isBackgroundVideoOwner(target);
2070
2071    // Capture Elementor position before any class adds.
2072    var preservePos = '';
2073    if (isBg) {
2074      try {
2075        preservePos = window.getComputedStyle(target).position || '';
2076      } catch (eRead) {
2077        preservePos = '';
2078      }
2079    }
2080
2081    var wrap = ensureWrap(target, mode);
2082    wrap.setAttribute('data-ucpf-guard-kind', kind);
2083    wrap.setAttribute('data-ucpf-guard-category', cats.join(','));
2084    wrap.classList.add('ucpf-consent-guard--active');
2085    if (isBg || wrap.classList.contains('ucpf-consent-guard--bg')) {
2086      wrap.classList.add('ucpf-consent-guard--bg');
2087      // Never apply --embed (relative/min-height/width) to Elementor absolute e-cons.
2088      wrap.classList.remove('ucpf-consent-guard--embed', 'ucpf-consent-guard--form');
2089      // Only invent a positioning context if Elementor left the host static.
2090      if (!preservePos || preservePos === 'static') {
2091        wrap.style.setProperty('position', 'relative', 'important');
2092        wrap.setAttribute('data-ucpf-bg-pos', '1');
2093      } else if (wrap.getAttribute('data-ucpf-bg-pos') === '1') {
2094        // Drop forced absolute from older builds; Elementor owns coords.
2095        wrap.style.removeProperty('position');
2096        wrap.removeAttribute('data-ucpf-bg-pos');
2097      }
2098    } else if (mode === 'embed') {
2099      // Measure before parking iframes so the glass overlay keeps the real media box.
2100      preserveEmbedBoxSize(wrap);
2101      if (wrap !== target) {
2102        preserveEmbedBoxSize(target);
2103      }
2104    }
2105    syncThemeOnto(wrap);
2106
2107    var panel = null;
2108    try {
2109      panel = wrap.querySelector(':scope > .ucpf-consent-guard__panel');
2110    } catch (eScope) {
2111      panel = null;
2112    }
2113    if (!panel) {
2114      var kids = wrap.children;
2115      for (var ci = 0; ci < kids.length; ci++) {
2116        if (kids[ci].classList && kids[ci].classList.contains('ucpf-consent-guard__panel')) {
2117          panel = kids[ci];
2118          break;
2119        }
2120      }
2121    }
2122    if (!panel) {
2123      panel = wrap.querySelector('.ucpf-consent-guard__panel');
2124    }
2125    var newPanel = buildPanel(kind, category, cats);
2126    try {
2127      if (panel && panel.parentNode === wrap) {
2128        wrap.replaceChild(newPanel, panel);
2129      } else {
2130        var anchor = wrap === target ? wrap.firstChild : target;
2131        if (anchor && anchor.parentNode === wrap) {
2132          wrap.insertBefore(newPanel, anchor);
2133        } else if (wrap.firstChild && wrap.firstChild.parentNode === wrap) {
2134          wrap.insertBefore(newPanel, wrap.firstChild);
2135        } else {
2136          wrap.appendChild(newPanel);
2137        }
2138      }
2139    } catch (eInsert) {
2140      try {
2141        wrap.appendChild(newPanel);
2142      } catch (eAppend) {
2143        /* ignore — Elementor may reparent mid-refresh */
2144      }
2145    }
2146
2147    if (mode === 'form') {
2148      lockFields(target);
2149    }
2150    if (isVideoKind || isBg) {
2151      parkVideoIframes(target);
2152      parkVideoIframes(wrap);
2153    }
2154    target.setAttribute('data-ucpf-consent-guarded', '1');
2155    guarded.set(target, key);
2156  }
2157
2158  function removeGuard(target) {
2159    if (!target || target.getAttribute('data-ucpf-consent-guarded') !== '1') {
2160      return;
2161    }
2162    if (target.tagName === 'FORM') {
2163      unlockFields(target);
2164    }
2165    restoreParkedVideoIframes(target);
2166    target.removeAttribute('data-ucpf-consent-guarded');
2167
2168    var wrap = guardHost(target) || (target.classList.contains('ucpf-consent-guard') ? target : null);
2169    if (wrap && wrap.classList.contains('ucpf-consent-guard')) {
2170      restoreParkedVideoIframes(wrap);
2171      clearEmbedBoxSize(wrap);
2172      clearEmbedBoxSize(target);
2173      var panel = wrap.querySelector('.ucpf-consent-guard__panel');
2174      if (panel) {
2175        panel.remove();
2176      }
2177      wrap.classList.remove('ucpf-consent-guard--active');
2178
2179      var inPlace = wrap.getAttribute('data-ucpf-guard-shell') === '1' || wrap === target;
2180      if (inPlace) {
2181        wrap.classList.remove(
2182          'ucpf-consent-guard',
2183          'ucpf-captcha-guard',
2184          'ucpf-consent-guard--embed',
2185          'ucpf-consent-guard--form',
2186          'ucpf-consent-guard--bg'
2187        );
2188        wrap.removeAttribute('data-ucpf-guard-shell');
2189        wrap.removeAttribute('data-ucpf-guard-kind');
2190        wrap.removeAttribute('data-ucpf-guard-category');
2191        if (wrap.getAttribute('data-ucpf-bg-pos') === '1') {
2192          wrap.style.removeProperty('position');
2193          wrap.removeAttribute('data-ucpf-bg-pos');
2194        }
2195      } else if (wrap.parentNode) {
2196        wrap.parentNode.insertBefore(target, wrap);
2197        wrap.remove();
2198      }
2199    } else {
2200      clearEmbedBoxSize(target);
2201    }
2202    guarded.set(target, 'off');
2203
2204    // Overlay is gone — remount the player immediately if restore left a blank shell.
2205    var videoHost =
2206      (target.classList &&
2207        (target.classList.contains('elementor-widget-video') ||
2208          target.getAttribute('data-widget_type') === 'video.default' ||
2209          target.classList.contains('elementor-wrapper'))) ||
2210      (target.querySelector &&
2211        target.querySelector('.elementor-wrapper.elementor-open-inline, iframe.elementor-video, iframe[data-src*="youtube"], iframe[data-src*="vimeo"]'));
2212    if (videoHost && hasCategoryConsent('marketing') && hasCategoryConsent('functional')) {
2213      var remountRoot =
2214        (target.closest && (target.closest('.elementor-widget-video') || target.closest('[data-widget_type="video.default"]'))) ||
2215        target;
2216      var kick = function (allowCreate) {
2217        restoreParkedVideoIframes(remountRoot);
2218        injectElementorOpenInlineVideo(remountRoot, { allowCreate: !!allowCreate });
2219        dedupeElementorOpenInlineVideos(remountRoot);
2220      };
2221      // Restore Elementor's frame first; create only if still empty shortly after.
2222      kick(false);
2223      window.setTimeout(function () {
2224        kick(false);
2225      }, 200);
2226      window.setTimeout(function () {
2227        kick(true);
2228      }, 1000);
2229    }
2230  }
2231
2232  function kindForPlaceholder(node) {
2233    var service = (node.getAttribute('data-ucpf-service') || '').toLowerCase();
2234    var src = (node.getAttribute('data-src') || node.getAttribute('src') || '').toLowerCase();
2235    if (service.indexOf('map') !== -1 || /maps\.google|google\.com\/maps|openstreetmap|mapbox|maplibre/.test(src)) {
2236      return 'map';
2237    }
2238    if (service.indexOf('youtube') !== -1 || src.indexOf('youtube') !== -1 || src.indexOf('youtu.be') !== -1) {
2239      return 'youtube';
2240    }
2241    if (service.indexOf('vimeo') !== -1 || src.indexOf('vimeo') !== -1) {
2242      return 'vimeo';
2243    }
2244    if (service.indexOf('calendly') !== -1 || src.indexOf('calendly.com') !== -1) {
2245      return 'calendly';
2246    }
2247    if (
2248      service.indexOf('jobber') !== -1 ||
2249      /getjobber\.com|clienthub\.getjobber|work_request|typeform\.com|jotform\.com|hsforms\.com|tally\.so/.test(src)
2250    ) {
2251      return 'widget';
2252    }
2253    return 'embed';
2254  }
2255
2256  function parseDataSettings(el) {
2257    if (!el || !el.getAttribute) {
2258      return null;
2259    }
2260    var raw = el.getAttribute('data-settings') || el.getAttribute('data-elementor-settings') || '';
2261    if (!raw) {
2262      return null;
2263    }
2264    try {
2265      return JSON.parse(raw);
2266    } catch (e1) {
2267      try {
2268        return JSON.parse(raw.replace(/&quot;/g, '"').replace(/&#039;/g, "'"));
2269      } catch (e2) {
2270        return null;
2271      }
2272    }
2273  }
2274
2275  /**
2276   * Detect YouTube / Vimeo from builder widgets even when the iframe is not painted yet.
2277   * @param {Element} el
2278   * @returns {{ kind: string, category: string }|null}
2279   */
2280  function hostMatchesPage(hostname) {
2281    var a = String(hostname || '')
2282      .replace(/^www\./i, '')
2283      .toLowerCase();
2284    var b = String(window.location.hostname || '')
2285      .replace(/^www\./i, '')
2286      .toLowerCase();
2287    return !!(a && b && a === b);
2288  }
2289
2290  /** Same-origin (or relative / blob / data) media — first-party, never consent-gated. */
2291  function isSameOriginMediaUrl(url) {
2292    var raw = String(url || '').trim();
2293    if (!raw) {
2294      return false;
2295    }
2296    if (/^(blob:|data:)/i.test(raw)) {
2297      return true;
2298    }
2299    if (raw.charAt(0) === '/' && raw.charAt(1) !== '/') {
2300      return true;
2301    }
2302    try {
2303      var u = new URL(raw, window.location.href);
2304      return hostMatchesPage(u.hostname);
2305    } catch (e) {
2306      return false;
2307    }
2308  }
2309
2310  /**
2311   * Elementor Self Hosted / HTML5 <video> / same-origin iframe — do not cover.
2312   * @param {Element} el
2313   * @return {boolean}
2314   */
2315  function isSelfHostedVideoSurface(el) {
2316    if (!el || el.nodeType !== 1) {
2317      return false;
2318    }
2319    var settings = parseDataSettings(el);
2320    if ((!settings || typeof settings !== 'object') && el.closest) {
2321      var host = el.closest('[data-settings]');
2322      settings = parseDataSettings(host);
2323    }
2324    if (settings && typeof settings === 'object') {
2325      var vtype = String(settings.video_type || '').toLowerCase();
2326      if (vtype === 'hosted' || vtype === 'self' || vtype === 'self_hosted' || vtype === 'file') {
2327        return true;
2328      }
2329      if (settings.hosted_url && isSameOriginMediaUrl(settings.hosted_url)) {
2330        return true;
2331      }
2332      var bg = String(settings.background_video_link || settings.background_background || '');
2333      if (bg && isSameOriginMediaUrl(bg) && /\.(mp4|webm|ogg|ogv|m4v)(\?|#|$)/i.test(bg)) {
2334        return true;
2335      }
2336    }
2337    if (el.tagName === 'VIDEO') {
2338      var vsrc = el.getAttribute('src') || el.getAttribute('data-src') || '';
2339      if (vsrc && isSameOriginMediaUrl(vsrc)) {
2340        return true;
2341      }
2342      var sources = el.querySelectorAll ? el.querySelectorAll('source[src], source[data-src]') : [];
2343      for (var si = 0; si < sources.length; si++) {
2344        var ss = sources[si].getAttribute('src') || sources[si].getAttribute('data-src') || '';
2345        if (ss && isSameOriginMediaUrl(ss)) {
2346          return true;
2347        }
2348      }
2349    }
2350    if (el.querySelector) {
2351      var video = el.querySelector('video[src], video source[src], video[data-src], video source[data-src]');
2352      if (video) {
2353        var nested =
2354          (video.tagName === 'SOURCE' ? video : video).getAttribute('src') ||
2355          (video.tagName === 'SOURCE' ? video : video).getAttribute('data-src') ||
2356          '';
2357        if (!nested && video.querySelector) {
2358          var nestedSrc = video.querySelector('source[src], source[data-src]');
2359          nested = nestedSrc
2360            ? nestedSrc.getAttribute('src') || nestedSrc.getAttribute('data-src') || ''
2361            : '';
2362        }
2363        if (nested && isSameOriginMediaUrl(nested)) {
2364          return true;
2365        }
2366        if (video.tagName === 'VIDEO' && isSelfHostedVideoSurface(video)) {
2367          return true;
2368        }
2369      }
2370    }
2371    if (el.tagName === 'IFRAME') {
2372      var iframeSrc = el.getAttribute('src') || el.getAttribute('data-src') || '';
2373      if (iframeSrc && isSameOriginMediaUrl(iframeSrc) && !isVideoPlayerUrl(iframeSrc)) {
2374        return true;
2375      }
2376    }
2377    return false;
2378  }
2379
2380  function detectVideoProvider(el) {
2381    if (!el || el.nodeType !== 1) {
2382      return null;
2383    }
2384    if (isSelfHostedVideoSurface(el)) {
2385      return null;
2386    }
2387
2388    var blob = '';
2389    var attrs = [
2390      'src',
2391      'data-src',
2392      'data-lazy-src',
2393      'data-youtube',
2394      'data-youtube-url',
2395      'data-vimeo-url',
2396      'data-vimeo-id',
2397      'href',
2398      'data-url',
2399    ];
2400    for (var a = 0; a < attrs.length; a++) {
2401      blob += ' ' + (el.getAttribute(attrs[a]) || '');
2402    }
2403    blob += ' ' + (el.className || '');
2404    blob += ' ' + (el.getAttribute('data-widget_type') || '');
2405
2406    var settings = parseDataSettings(el);
2407    if (settings && typeof settings === 'object') {
2408      var vtypeEarly = String(settings.video_type || '').toLowerCase();
2409      if (vtypeEarly === 'hosted' || vtypeEarly === 'self' || vtypeEarly === 'self_hosted' || vtypeEarly === 'file') {
2410        return null;
2411      }
2412      blob +=
2413        ' ' +
2414        (settings.video_type || '') +
2415        ' ' +
2416        (settings.youtube_url || '') +
2417        ' ' +
2418        (settings.vimeo_url || '') +
2419        ' ' +
2420        (settings.dailymotion_url || '') +
2421        ' ' +
2422        (settings.hosted_url || '') +
2423        ' ' +
2424        (settings.background_video_link || '') +
2425        ' ' +
2426        (settings.background_background || '');
2427    }
2428
2429    // Walk parents for Elementor widget / container data-settings (background videos live on e-con).
2430    if (el.parentElement) {
2431      var settingsHost = el.closest
2432        ? el.closest('[data-settings*="background_video"], [data-settings*="youtube_url"], [data-settings*="vimeo_url"], [data-settings]')
2433        : el.parentElement;
2434      var parentSettings = parseDataSettings(el.parentElement) || parseDataSettings(settingsHost);
2435      if (parentSettings && typeof parentSettings === 'object') {
2436        var pv = String(parentSettings.video_type || '').toLowerCase();
2437        if (pv === 'hosted' || pv === 'self' || pv === 'self_hosted' || pv === 'file') {
2438          return null;
2439        }
2440        blob +=
2441          ' ' +
2442          (parentSettings.video_type || '') +
2443          ' ' +
2444          (parentSettings.youtube_url || '') +
2445          ' ' +
2446          (parentSettings.vimeo_url || '') +
2447          ' ' +
2448          (parentSettings.background_video_link || '') +
2449          ' ' +
2450          (parentSettings.background_background || '');
2451      }
2452    }
2453
2454    blob = blob.toLowerCase();
2455    // Hosted-only blob with no third-party player signal.
2456    if (/hosted/.test(blob) && !/youtube|youtu\.be|vimeo|dailymotion/.test(blob)) {
2457      return null;
2458    }
2459    if (/vimeo|player\.vimeo/.test(blob)) {
2460      return { kind: 'vimeo', category: 'functional' };
2461    }
2462    if (/youtube|youtu\.be|youtube-nocookie/.test(blob)) {
2463      return { kind: 'youtube', category: 'marketing' };
2464    }
2465    return null;
2466  }
2467
2468  function calendlyHostContainer(node) {
2469    if (!node || node.nodeType !== 1) {
2470      return null;
2471    }
2472    if (node.classList && (node.classList.contains('calendly-inline-widget') || node.classList.contains('calendly-badge-widget'))) {
2473      return node.closest('.elementor-widget-container') || node.closest('.elementor-widget') || node;
2474    }
2475    if (node.tagName === 'IFRAME' && node.parentElement) {
2476      return (
2477        node.closest('.calendly-inline-widget') ||
2478        node.closest('.elementor-widget-container') ||
2479        node.parentElement
2480      );
2481    }
2482    var dataUrl = (node.getAttribute('data-url') || '').toLowerCase();
2483    if (dataUrl.indexOf('calendly.com') !== -1) {
2484      return node.closest('.elementor-widget-container') || node.closest('.elementor-widget') || node;
2485    }
2486    return node;
2487  }
2488
2489  /**
2490   * Prefer a sized mount box for Jobber / Typeform / Elementor HTML embeds.
2491   *
2492   * @param {Element} node
2493   * @return {Element|null}
2494   */
2495  function widgetEmbedHostContainer(node) {
2496    if (!node || node.nodeType !== 1) {
2497      return null;
2498    }
2499    // UserWay accessibility toolbar — never promote a cover host.
2500    if (isUserWayNode(node) || isUserWayUrl((node.getAttribute('data-src') || '') + ' ' + (node.getAttribute('src') || ''))) {
2501      return null;
2502    }
2503    if (node.classList && node.classList.contains('jobber-inline-work-request')) {
2504      return node;
2505    }
2506    var blob = (
2507      (node.getAttribute('data-src') || '') +
2508      ' ' +
2509      (node.getAttribute('src') || '') +
2510      ' ' +
2511      (node.id || '')
2512    ).toLowerCase();
2513    // Calendly script often lives under <body> — cover the widget box, never the page.
2514    if (blob.indexOf('calendly') !== -1 && node.ownerDocument) {
2515      var cal =
2516        (node.closest && node.closest('.calendly-inline-widget, .calendly-badge-widget')) ||
2517        node.ownerDocument.querySelector('.calendly-inline-widget[data-url], .calendly-badge-widget, .calendly-inline-widget');
2518      if (cal && !isForbiddenGuardHost(cal)) {
2519        return cal;
2520      }
2521    }
2522    if (node.closest) {
2523      // Prefer the Elementor / builder layout box — not the parked iframe (collapses to 0).
2524      var box =
2525        node.closest('[data-widget_type="html.default"]') ||
2526        node.closest('.elementor-widget-html') ||
2527        node.closest('[data-widget_type="tp-meeting-scheduler.default"]') ||
2528        node.closest('.elementor-widget-tp-meeting-scheduler') ||
2529        node.closest('.jobber-inline-work-request') ||
2530        node.closest('[class*="jobber-inline"]') ||
2531        node.closest('.elementor-widget-container') ||
2532        node.closest('.elementor-widget');
2533      if (box && !isForbiddenGuardHost(box)) {
2534        return box;
2535      }
2536    }
2537    if (node.tagName === 'IFRAME' && node.parentElement && !isForbiddenGuardHost(node.parentElement)) {
2538      return node.parentElement;
2539    }
2540    // Scripts parked under <body>/<head> must not promote the document chrome as a host.
2541    if (node.tagName === 'SCRIPT') {
2542      return null;
2543    }
2544    if (isForbiddenGuardHost(node)) {
2545      return null;
2546    }
2547    return node;
2548  }
2549
2550  /**
2551   * Whether a gated script is a form/widget embed (not analytics).
2552   *
2553   * @param {Element} script
2554   * @return {boolean}
2555   */
2556  function isEmbedLikeScript(script) {
2557    if (!script || script.tagName !== 'SCRIPT') {
2558      return false;
2559    }
2560    var blob = (
2561      (script.getAttribute('data-src') || '') +
2562      ' ' +
2563      (script.getAttribute('src') || '') +
2564      ' ' +
2565      (script.getAttribute('form_url') || '') +
2566      ' ' +
2567      (script.getAttribute('typehub_id') || '') +
2568      ' ' +
2569      (script.id || '') +
2570      ' ' +
2571      (script.className || '')
2572    ).toLowerCase();
2573    // Accessibility toolbar — never treat as a third-party form/widget cover.
2574    if (isUserWayUrl(blob) || blob.indexOf('userway') !== -1) {
2575      return false;
2576    }
2577    if (
2578      /(google-analytics|googletagmanager|gtag\/js|facebook\.net|fbevents|hotjar|clarity\.ms|doubleclick|googlesyndication)/.test(
2579        blob
2580      )
2581    ) {
2582      return false;
2583    }
2584    if (
2585      /(jobber|getjobber|work_request|typeform|jotform|hsforms|hubspot\.com\/.*form|calendly|wufoo|fillout\.com|tally\.so|forms\.office)/.test(
2586        blob
2587      )
2588    ) {
2589      return true;
2590    }
2591    if (script.getAttribute('form_url') || script.getAttribute('typehub_id') || script.getAttribute('data-form-id')) {
2592      return true;
2593    }
2594    // Elementor HTML widgets often host third-party form snippets.
2595    if (script.closest) {
2596      return !!(
2597        script.closest('.elementor-widget-html') ||
2598        script.closest('[data-widget_type="html.default"]') ||
2599        script.closest('.jobber-inline-work-request')
2600      );
2601    }
2602    return false;
2603  }
2604
2605  /**
2606   * Calendly only scans the DOM once on script load. Elementor popups inject
2607   * .calendly-inline-widget later — re-run init after Functional consent.
2608   */
2609  function reinitCalendlyWidgets() {
2610    if (!hasCategoryConsent('functional')) {
2611      return;
2612    }
2613    if (window.UCPFLoader && typeof window.UCPFLoader.applyConsent === 'function') {
2614      try {
2615        window.UCPFLoader.applyConsent();
2616      } catch (eLoad) {
2617        /* ignore */
2618      }
2619    }
2620    var attempt = 0;
2621    function tryInit() {
2622      attempt += 1;
2623      var C = window.Calendly;
2624      var nodes = document.querySelectorAll('.calendly-inline-widget[data-url], .calendly-badge-widget[data-url]');
2625      if (!nodes.length) {
2626        return;
2627      }
2628      if (C && typeof C === 'object') {
2629        try {
2630          if (typeof C.initInlineWidgets === 'function') {
2631            C.initInlineWidgets();
2632            return;
2633          }
2634        } catch (eInitAll) {
2635          /* fall through per-node */
2636        }
2637        Array.prototype.forEach.call(nodes, function (el) {
2638          if (el.querySelector && el.querySelector('iframe')) {
2639            return;
2640          }
2641          var url = el.getAttribute('data-url');
2642          if (!url) {
2643            return;
2644          }
2645          try {
2646            if (typeof C.initInlineWidget === 'function') {
2647              C.initInlineWidget({ url: url, parentElement: el });
2648            }
2649          } catch (eOne) {
2650            /* ignore */
2651          }
2652        });
2653        return;
2654      }
2655      if (attempt < 20) {
2656        window.setTimeout(tryInit, 250);
2657      }
2658    }
2659    tryInit();
2660  }
2661
2662  /**
2663   * Extract a YouTube video id from watch / embed / shorts / youtu.be URLs.
2664   * @param {string} raw
2665   * @return {string}
2666   */
2667  function youtubeIdFromUrl(raw) {
2668    var url = String(raw || '').trim();
2669    if (!url) {
2670      return '';
2671    }
2672    var m =
2673      url.match(/youtu\.be\/([A-Za-z0-9_-]{6,})/i) ||
2674      url.match(/youtube\.com\/shorts\/([A-Za-z0-9_-]{6,})/i) ||
2675      url.match(/youtube(?:-nocookie)?\.com\/embed\/([A-Za-z0-9_-]{6,})/i) ||
2676      url.match(/[?&]v=([A-Za-z0-9_-]{6,})/i) ||
2677      url.match(/youtube\.com\/live\/([A-Za-z0-9_-]{6,})/i);
2678    return m && m[1] ? m[1] : '';
2679  }
2680
2681  /**
2682   * @param {string} raw
2683   * @param {object} [settings]
2684   * @return {string}
2685   */
2686  function youtubeEmbedSrc(raw, settings) {
2687    var id = youtubeIdFromUrl(raw);
2688    if (!id) {
2689      return '';
2690    }
2691    var params = ['feature=oembed', 'enablejsapi=1'];
2692    var rawStr = String(raw || '');
2693    var preserved = {};
2694    try {
2695      var q = rawStr.split('?')[1] || '';
2696      if (q) {
2697        q.replace(/^\?/, '')
2698          .split('&')
2699          .forEach(function (pair) {
2700            var parts = pair.split('=');
2701            var k = decodeURIComponent(parts[0] || '').toLowerCase();
2702            var v = decodeURIComponent((parts[1] || '').replace(/\+/g, ' '));
2703            if (k === 'list' || k === 'start' || k === 'si' || k === 'index') {
2704              preserved[k] = v;
2705            }
2706            if (k === 't' || k === 'time_continue') {
2707              var secs = v;
2708              if (/^\d+$/.test(v)) {
2709                preserved.start = v;
2710              } else {
2711                var tm = String(v).match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/i);
2712                if (tm) {
2713                  preserved.start = String(
2714                    (parseInt(tm[1] || '0', 10) || 0) * 3600 +
2715                      (parseInt(tm[2] || '0', 10) || 0) * 60 +
2716                      (parseInt(tm[3] || '0', 10) || 0)
2717                  );
2718                }
2719              }
2720            }
2721          });
2722      }
2723    } catch (eYtQ) { /* ignore */ }
2724
2725    Object.keys(preserved).forEach(function (k) {
2726      if (preserved[k] !== '' && preserved[k] != null) {
2727        params.push(encodeURIComponent(k) + '=' + encodeURIComponent(preserved[k]));
2728      }
2729    });
2730
2731    if (settings && String(settings.controls) === 'yes') {
2732      params.push('controls=1');
2733    }
2734    if (settings && (settings.autoplay === 'yes' || settings.autoplay === true)) {
2735      params.push('autoplay=1');
2736      if (params.indexOf('mute=1') === -1) {
2737        params.push('mute=1');
2738      }
2739    }
2740    if (settings && (settings.mute === 'yes' || settings.mute === true)) {
2741      if (params.indexOf('mute=1') === -1) {
2742        params.push('mute=1');
2743      }
2744    }
2745    if (settings && (settings.loop === 'yes' || settings.loop === true)) {
2746      params.push('loop=1');
2747      if (!preserved.list) {
2748        params.push('playlist=' + encodeURIComponent(id));
2749      }
2750    }
2751    return 'https://www.youtube-nocookie.com/embed/' + encodeURIComponent(id) + '?' + params.join('&');
2752  }
2753
2754  /**
2755   * Build a Vimeo player URL without dropping unlisted privacy hashes.
2756   * Accepts vimeo.com/{id}/{hash}, ?h=, and player.vimeo.com/video/{id}?h=.
2757   *
2758   * @param {string} raw
2759   * @return {string}
2760   */
2761  function vimeoEmbedSrc(raw) {
2762    var url = String(raw || '').trim();
2763    if (!url) {
2764      return '';
2765    }
2766
2767    function hashFromQuery(u) {
2768      var m = String(u).match(/[?&#]h=([a-zA-Z0-9]+)/i);
2769      return m && m[1] ? m[1] : '';
2770    }
2771
2772    var playerM = url.match(/player\.vimeo\.com\/video\/(\d+)/i);
2773    if (playerM && playerM[1]) {
2774      var ph = hashFromQuery(url);
2775      return (
2776        'https://player.vimeo.com/video/' +
2777        encodeURIComponent(playerM[1]) +
2778        (ph ? '?h=' + encodeURIComponent(ph) : '')
2779      );
2780    }
2781
2782    // vimeo.com/1027049072/366bb7c771 or vimeo.com/video/123
2783    var pathM = url.match(/vimeo\.com\/(?:video\/)?(\d+)(?:\/([a-zA-Z0-9]+))?/i);
2784    if (!pathM || !pathM[1]) {
2785      return '';
2786    }
2787    var id = pathM[1];
2788    var hash = pathM[2] || hashFromQuery(url);
2789    // Path segments that are not privacy hashes.
2790    if (hash && /^(channels|manage|album|groups|ondemand|showcase|event|review|staffpick)$/i.test(hash)) {
2791      hash = hashFromQuery(url);
2792    }
2793    return (
2794      'https://player.vimeo.com/video/' +
2795      encodeURIComponent(id) +
2796      (hash ? '?h=' + encodeURIComponent(hash) : '')
2797    );
2798  }
2799
2800  /**
2801   * Prefer an exact existing iframe URL over rebuilding (keeps h=, list, etc.).
2802   *
2803   * @param {Element} root
2804   * @return {string}
2805   */
2806  function existingEmbedUrl(root) {
2807    if (!root || !root.querySelector) {
2808      return '';
2809    }
2810    var iframe = root.querySelector(
2811      'iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="youtube-nocookie"], iframe[src*="vimeo"],' +
2812        'iframe[data-src*="youtube"], iframe[data-src*="youtu.be"], iframe[data-src*="youtube-nocookie"], iframe[data-src*="vimeo"]'
2813    );
2814    if (!iframe) {
2815      return '';
2816    }
2817    return String(iframe.getAttribute('src') || iframe.getAttribute('data-src') || '').trim();
2818  }
2819
2820  /**
2821   * Build a plain iframe for non-Elementor empty shells only.
2822   * Do not invent Elementor background cover CSS — that breaks native styling.
2823   *
2824   * @param {string} src
2825   * @param {string} title
2826   * @return {HTMLIFrameElement}
2827   */
2828  function createConsentVideoIframe(src, title) {
2829    var iframe = document.createElement('iframe');
2830    iframe.className = 'elementor-video-iframe';
2831    iframe.setAttribute('src', src);
2832    iframe.setAttribute('title', title || 'Video player');
2833    iframe.setAttribute('frameborder', '0');
2834    iframe.setAttribute(
2835      'allow',
2836      'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen'
2837    );
2838    iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
2839    iframe.style.cssText = 'width:100%;height:100%;border:0;';
2840    return iframe;
2841  }
2842
2843  /**
2844   * Elementor-style cover size (px width/height), matching disabled-plugin markup.
2845   *
2846   * @param {HTMLIFrameElement} iframe
2847   * @param {Element} container
2848   */
2849  function sizeElementorBackgroundIframe(iframe, container) {
2850    var cw = (container && (container.clientWidth || container.offsetWidth)) || 0;
2851    var ch = (container && (container.clientHeight || container.offsetHeight)) || 0;
2852    if (cw < 2 || ch < 2) {
2853      cw = window.innerWidth || 1920;
2854      ch = window.innerHeight || 1080;
2855    }
2856    var ratio = 16 / 9;
2857    var width;
2858    var height;
2859    if (cw / ch > ratio) {
2860      width = cw;
2861      height = cw / ratio;
2862    } else {
2863      height = ch;
2864      width = ch * ratio;
2865    }
2866    // Slight overscan like Elementor's Vimeo background sizing.
2867    width = Math.ceil(width * 1.11);
2868    height = Math.ceil(height * 1.11);
2869    iframe.setAttribute('width', '426');
2870    iframe.setAttribute('height', '240');
2871    iframe.style.width = width + 'px';
2872    iframe.style.height = height + 'px';
2873  }
2874
2875  /**
2876   * Consent/park races can leave Elementor's BG shell with a live iframe but still
2877   * elementor-loading + elementor-invisible (blank hero). Heal without adding a second iframe.
2878   * Elementor may re-add those classes if its YT onReady never fired — CSS + observer stick.
2879   *
2880   * @param {Element} box
2881   * @param {HTMLIFrameElement} [iframe]
2882   * @return {boolean}
2883   */
2884  function healElementorBackgroundVideo(box, iframe) {
2885    if (!box) {
2886      return false;
2887    }
2888    var live =
2889      iframe ||
2890      box.querySelector('iframe.elementor-background-video-embed[src]:not(.elementor-video-iframe)') ||
2891      box.querySelector('iframe.elementor-background-video-embed[src]');
2892    if (!live) {
2893      return false;
2894    }
2895    var src = '';
2896    try {
2897      src = live.getAttribute('src') || live.src || '';
2898    } catch (eSrc) {
2899      src = '';
2900    }
2901    if (!src || src === 'about:blank' || /^javascript:/i.test(src)) {
2902      return false;
2903    }
2904    try {
2905      box.classList.remove('elementor-loading', 'elementor-invisible');
2906      box.classList.add('ucpf-bg-video-ready');
2907      box.setAttribute('data-ucpf-bg-healed', '1');
2908    } catch (eCls) { /* ignore */ }
2909    try {
2910      // Belt-and-suspenders: Elementor CSS uses visibility:hidden on .elementor-invisible.
2911      box.style.visibility = 'visible';
2912      box.style.opacity = '1';
2913    } catch (eStyle) { /* ignore */ }
2914    try {
2915      sizeElementorBackgroundIframe(live, box);
2916    } catch (eSize) { /* ignore */ }
2917    return true;
2918  }
2919
2920  /**
2921   * Scan every Elementor background-video shell with a live player and heal stuck loading state.
2922   *
2923   * @return {number}
2924   */
2925  function healAllElementorBackgroundVideos() {
2926    var n = 0;
2927    try {
2928      queryAll(['.elementor-background-video-container']).forEach(function (box) {
2929        if (healElementorBackgroundVideo(box)) {
2930          n += 1;
2931        }
2932      });
2933    } catch (eAll) { /* ignore */ }
2934    return n;
2935  }
2936
2937  var bgVideoHealObserver = null;
2938
2939  /**
2940   * If Elementor re-adds loading/invisible after a missed YT ready, strip again.
2941   */
2942  function watchElementorBackgroundVideos() {
2943    if (bgVideoHealObserver || typeof MutationObserver !== 'function') {
2944      return;
2945    }
2946    try {
2947      bgVideoHealObserver = new MutationObserver(function (records) {
2948        var need = false;
2949        for (var i = 0; i < records.length; i++) {
2950          var t = records[i].target;
2951          if (
2952            t &&
2953            t.classList &&
2954            t.classList.contains('elementor-background-video-container') &&
2955            (t.classList.contains('elementor-loading') || t.classList.contains('elementor-invisible'))
2956          ) {
2957            need = true;
2958            break;
2959          }
2960        }
2961        if (need) {
2962          healAllElementorBackgroundVideos();
2963        }
2964      });
2965      bgVideoHealObserver.observe(document.documentElement, {
2966        subtree: true,
2967        attributes: true,
2968        attributeFilter: ['class'],
2969      });
2970    } catch (eObs) {
2971      bgVideoHealObserver = null;
2972    }
2973  }
2974
2975  /**
2976   * Player URL matching Elementor's background Vimeo/YouTube iframe (keep privacy hash).
2977   *
2978   * @param {string} link
2979   * @return {{src:string,isVimeo:boolean}}
2980   */
2981  function elementorBackgroundPlayerUrl(link) {
2982    var isVimeo = /vimeo/i.test(String(link || ''));
2983    var src = '';
2984    if (isVimeo) {
2985      src = vimeoEmbedSrc(link);
2986      if (src) {
2987        var join = src.indexOf('?') === -1 ? '?' : '&';
2988        // Same query shape Elementor emits (plus app_id Elementor adds).
2989        if (src.indexOf('autoplay=') === -1) {
2990          src += join + 'muted=1&autoplay=1&loop=1&background=1&app_id=122963';
2991        } else if (src.indexOf('app_id=') === -1) {
2992          src += '&app_id=122963';
2993        }
2994      }
2995    } else {
2996      src = youtubeEmbedSrc(link, { autoplay: 'yes', mute: 'yes', loop: 'yes' });
2997    }
2998    return { src: src, isVimeo: isVimeo };
2999  }
3000
3001  /**
3002   * Inject background iframe that mirrors Elementor-disabled markup:
3003   * keep presentation div; sibling iframe.elementor-background-video-embed with px size.
3004   *
3005   * @param {Element} owner
3006   * @return {boolean}
3007   */
3008  function injectElementorNativeBackground(owner) {
3009    if (!owner) {
3010      return false;
3011    }
3012    var box = owner.querySelector('.elementor-background-video-container');
3013    if (!box) {
3014      return false;
3015    }
3016    // Already has a native Elementor iframe (not our old custom hydrate).
3017    var existing = box.querySelector('iframe.elementor-background-video-embed:not(.elementor-video-iframe)');
3018    if (existing && existing.getAttribute('src') && box.getAttribute('data-vimeo-initialized') === 'true') {
3019      return false;
3020    }
3021
3022    var settings = parseDataSettings(owner) || {};
3023    var link = String(settings.background_video_link || '');
3024    if (!link) {
3025      return false;
3026    }
3027    var isVimeo = /vimeo/i.test(link);
3028    if (isVimeo && !hasCategoryConsent('functional')) {
3029      return false;
3030    }
3031    if (!isVimeo && !(hasCategoryConsent('marketing') && hasCategoryConsent('functional'))) {
3032      return false;
3033    }
3034
3035    // Remove prior UCPF hydrates only.
3036    box.querySelectorAll('iframe.elementor-video-iframe').forEach(function (iframe) {
3037      iframe.remove();
3038    });
3039    // If a broken/partial iframe remains without native class sizing, replace it.
3040    box.querySelectorAll('iframe').forEach(function (iframe) {
3041      if (iframe.classList.contains('elementor-video-iframe')) {
3042        iframe.remove();
3043      }
3044    });
3045    if (box.querySelector('iframe.elementor-background-video-embed')) {
3046      return false;
3047    }
3048
3049    var placeholder = box.querySelector('div.elementor-background-video-embed');
3050    if (!placeholder) {
3051      placeholder = document.createElement('div');
3052      placeholder.className = 'elementor-background-video-embed';
3053      placeholder.setAttribute('role', 'presentation');
3054      box.insertBefore(placeholder, box.firstChild);
3055    } else if (!placeholder.getAttribute('role')) {
3056      placeholder.setAttribute('role', 'presentation');
3057    }
3058
3059    var built = elementorBackgroundPlayerUrl(link);
3060    if (!built.src) {
3061      return false;
3062    }
3063
3064    var iframe = document.createElement('iframe');
3065    iframe.className = 'elementor-background-video-embed';
3066    iframe.setAttribute('src', built.src);
3067    iframe.setAttribute('frameborder', '0');
3068    iframe.setAttribute(
3069      'allow',
3070      'autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share'
3071    );
3072    iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
3073    iframe.setAttribute('title', 'Background video');
3074    iframe.setAttribute('data-ready', 'true');
3075    sizeElementorBackgroundIframe(iframe, box);
3076    box.appendChild(iframe);
3077    box.setAttribute('data-vimeo-initialized', 'true');
3078    box.setAttribute('data-ucpf-bg-native', '1');
3079    owner.setAttribute('data-ucpf-bg-native', '1');
3080    owner.removeAttribute('data-ucpf-video-hydrated');
3081    box.removeAttribute('data-ucpf-video-hydrated');
3082    healElementorBackgroundVideo(box, iframe);
3083    return true;
3084  }
3085
3086  /**
3087   * Undo UCPF-injected background iframes and restore Elementor's empty embed shell.
3088   *
3089   * @param {Element} owner Container with background_video_link settings.
3090   */
3091  function restoreElementorBackgroundShell(owner) {
3092    if (!owner) {
3093      return null;
3094    }
3095    var box = owner.querySelector('.elementor-background-video-container');
3096    if (!box) {
3097      return null;
3098    }
3099    try {
3100      box.querySelectorAll('iframe.elementor-video-iframe').forEach(function (iframe) {
3101        iframe.remove();
3102      });
3103      var placeholder = box.querySelector('.elementor-background-video-embed[role="presentation"]');
3104      if (!placeholder) {
3105        placeholder = box.querySelector('div.elementor-background-video-embed:not(iframe)');
3106      }
3107      if (!placeholder) {
3108        placeholder = document.createElement('div');
3109        placeholder.className = 'elementor-background-video-embed';
3110        placeholder.setAttribute('role', 'presentation');
3111        box.insertBefore(placeholder, box.firstChild);
3112      }
3113      box.removeAttribute('data-ucpf-video-hydrated');
3114      owner.removeAttribute('data-ucpf-video-hydrated');
3115    } catch (eRestore) { /* ignore */ }
3116    return box;
3117  }
3118
3119  function whenPlayerApiReady(isVimeo, cb) {
3120    var ready = function () {
3121      if (isVimeo) {
3122        return typeof window.Vimeo !== 'undefined';
3123      }
3124      return typeof window.YT !== 'undefined' || true;
3125    };
3126    if (ready()) {
3127      cb();
3128      return;
3129    }
3130    var n = 0;
3131    var t = window.setInterval(function () {
3132      n += 1;
3133      if (ready() || n > 50) {
3134        window.clearInterval(t);
3135        cb();
3136      }
3137    }, 100);
3138  }
3139
3140  /**
3141   * Live YouTube / Vimeo iframe already playing inside a root.
3142   *
3143   * @param {Element} root
3144   * @return {boolean}
3145   */
3146  function hasLiveVideoPlayer(root) {
3147    if (!root || !root.querySelector) {
3148      return false;
3149    }
3150    return !!root.querySelector(
3151      'iframe[src*="youtube.com"], iframe[src*="youtu.be"], iframe[src*="youtube-nocookie.com"], iframe[src*="player.vimeo.com"], iframe[src*="vimeo.com"]'
3152    );
3153  }
3154
3155  /**
3156   * Prefer a single Elementor-native player. Our late inject must not stack a
3157   * second iframe.elementor-video next to Elementor's (hard-refresh showed both).
3158   *
3159   * @param {Element} [root] Widget or document.
3160   */
3161  function dedupeElementorOpenInlineVideos(root) {
3162    var scope = root && root.querySelectorAll ? root : document;
3163    var widgets = [];
3164    if (
3165      scope.classList &&
3166      (scope.classList.contains('elementor-widget-video') || scope.getAttribute('data-widget_type') === 'video.default')
3167    ) {
3168      widgets = [scope];
3169    } else {
3170      widgets = Array.prototype.slice.call(
3171        scope.querySelectorAll
3172          ? scope.querySelectorAll('.elementor-widget-video, [data-widget_type="video.default"]')
3173          : []
3174      );
3175    }
3176    widgets.forEach(function (widget) {
3177      var wrap = widget.querySelector('.elementor-wrapper.elementor-open-inline') || widget;
3178      if (!wrap || !wrap.querySelectorAll) {
3179        return;
3180      }
3181      var iframes = Array.prototype.slice.call(
3182        wrap.querySelectorAll('iframe.elementor-video, iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="youtube-nocookie"], iframe[src*="vimeo"]')
3183      );
3184      if (iframes.length <= 1) {
3185        return;
3186      }
3187      var keep = null;
3188      // 1) Live Elementor player (not our inject).
3189      iframes.forEach(function (iframe) {
3190        if (keep) {
3191          return;
3192        }
3193        var src = iframe.getAttribute('src') || '';
3194        if (
3195          iframe.getAttribute('data-ucpf-injected-video') !== '1' &&
3196          src &&
3197          src !== 'about:blank' &&
3198          isVideoPlayerUrl(src)
3199        ) {
3200          keep = iframe;
3201        }
3202      });
3203      // 2) Any live player.
3204      if (!keep) {
3205        iframes.forEach(function (iframe) {
3206          if (keep) {
3207            return;
3208          }
3209          var src = iframe.getAttribute('src') || '';
3210          if (src && src !== 'about:blank' && isVideoPlayerUrl(src)) {
3211            keep = iframe;
3212          }
3213        });
3214      }
3215      // 3) Non-injected shell (parked Elementor frame).
3216      if (!keep) {
3217        iframes.forEach(function (iframe) {
3218          if (keep) {
3219            return;
3220          }
3221          if (iframe.getAttribute('data-ucpf-injected-video') !== '1') {
3222            keep = iframe;
3223          }
3224        });
3225      }
3226      if (!keep) {
3227        keep = iframes[0];
3228      }
3229      iframes.forEach(function (iframe) {
3230        if (iframe !== keep) {
3231          try {
3232            iframe.remove();
3233          } catch (eRm) { /* ignore */ }
3234        }
3235      });
3236    });
3237  }
3238
3239  /**
3240   * Additional method for Elementor Video widgets in open-inline mode.
3241   * Restores the existing iframe.elementor-video when possible; only creates a
3242   * new frame when the shell is empty (and marks it so we can dedupe later).
3243   *
3244   * @param {Element} widget
3245   * @param {{ allowCreate?: boolean }} [opts]
3246   * @return {boolean}
3247   */
3248  function injectElementorOpenInlineVideo(widget, opts) {
3249    opts = opts || {};
3250    var allowCreate = opts.allowCreate !== false;
3251
3252    if (!widget || !widget.querySelector) {
3253      return false;
3254    }
3255    if (widget.querySelector('.elementor-background-video-container') || isBackgroundVideoOwner(widget)) {
3256      return false;
3257    }
3258    if (isSelfHostedVideoSurface(widget)) {
3259      return false;
3260    }
3261
3262    var wrap = widget.querySelector('.elementor-wrapper.elementor-open-inline');
3263    if (!wrap) {
3264      return false;
3265    }
3266
3267    // Already have a live player — only strip duplicates.
3268    if (hasLiveVideoPlayer(wrap) || hasLiveVideoPlayer(widget)) {
3269      dedupeElementorOpenInlineVideos(widget);
3270      return true;
3271    }
3272
3273    var settings = parseDataSettings(widget) || {};
3274    var videoType = String(settings.video_type || '').toLowerCase();
3275    if (videoType === 'hosted') {
3276      return false;
3277    }
3278
3279    var ytLink = String(settings.youtube_url || '');
3280    var vimLink = String(settings.vimeo_url || '');
3281    var isVimeo =
3282      videoType === 'vimeo' ||
3283      (!!vimLink && !ytLink) ||
3284      /vimeo/i.test(vimLink || existingEmbedUrl(wrap) || existingEmbedUrl(widget));
3285
3286    if (isVimeo) {
3287      if (!hasCategoryConsent('functional')) {
3288        return false;
3289      }
3290    } else if (!(hasCategoryConsent('marketing') && hasCategoryConsent('functional'))) {
3291      return false;
3292    }
3293
3294    // Prefer Elementor's exact parked URL (youtube.com/…) over rebuilding nocookie.
3295    var src = existingEmbedUrl(wrap) || existingEmbedUrl(widget);
3296    if (!src || !isVideoPlayerUrl(src)) {
3297      if (isVimeo) {
3298        src = vimeoEmbedSrc(vimLink || ytLink);
3299      } else {
3300        src = youtubeEmbedSrc(ytLink || vimLink, settings);
3301      }
3302    }
3303    if (!src) {
3304      return false;
3305    }
3306
3307    var iframe =
3308      wrap.querySelector('iframe.elementor-video:not([data-ucpf-injected-video])') ||
3309      wrap.querySelector('iframe.elementor-video') ||
3310      wrap.querySelector('iframe[data-ucpf-parked="1"]') ||
3311      wrap.querySelector('iframe[data-src]') ||
3312      wrap.querySelector('iframe');
3313
3314    if (iframe) {
3315      if (!iframe.classList.contains('elementor-video')) {
3316        iframe.classList.add('elementor-video');
3317      }
3318      if (!forceVideoIframeSrc(iframe, src)) {
3319        return false;
3320      }
3321      dedupeElementorOpenInlineVideos(widget);
3322      return true;
3323    }
3324
3325    // Empty shell only — wait for Elementor unless allowCreate (late fallback).
3326    if (!allowCreate) {
3327      return false;
3328    }
3329
3330    iframe = document.createElement('iframe');
3331    iframe.className = 'elementor-video';
3332    iframe.setAttribute('data-ucpf-injected-video', '1');
3333    iframe.setAttribute('frameborder', '0');
3334    iframe.setAttribute('allowfullscreen', '');
3335    iframe.setAttribute(
3336      'allow',
3337      'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share'
3338    );
3339    iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
3340    iframe.setAttribute('title', 'Video player');
3341    iframe.setAttribute('width', '640');
3342    iframe.setAttribute('height', '360');
3343    wrap.appendChild(iframe);
3344    if (!forceVideoIframeSrc(iframe, src)) {
3345      try {
3346        iframe.remove();
3347      } catch (eRm) { /* ignore */ }
3348      return false;
3349    }
3350
3351    widget.setAttribute('data-ucpf-open-inline', '1');
3352    wrap.setAttribute('data-ucpf-open-inline', '1');
3353    dedupeElementorOpenInlineVideos(widget);
3354    return true;
3355  }
3356
3357  /**
3358   * After consent: try Elementor handlers, then native-matching iframe fallback.
3359   *
3360   * @return {boolean}
3361   */
3362  function reinitElementorVideos() {
3363    var hasEl =
3364      !!(
3365        window.elementorFrontend &&
3366        elementorFrontend.elementsHandler &&
3367        typeof elementorFrontend.elementsHandler.runReadyTrigger === 'function'
3368      );
3369    var run = function (el) {
3370      if (!hasEl) {
3371        return;
3372      }
3373      try {
3374        elementorFrontend.elementsHandler.runReadyTrigger(el);
3375      } catch (eRun) { /* ignore */ }
3376    };
3377
3378    queryAll(['[data-settings*="background_video_link"]']).forEach(function (owner) {
3379      var box = owner.querySelector('.elementor-background-video-container');
3380      if (!box) {
3381        return;
3382      }
3383      // Native Elementor success — clear stuck loading/invisible, then leave alone.
3384      if (
3385        owner.getAttribute('data-ucpf-bg-native') === '1' ||
3386        box.getAttribute('data-ucpf-bg-native') === '1'
3387      ) {
3388        var native = box.querySelector('iframe.elementor-background-video-embed:not(.elementor-video-iframe)');
3389        if (native && native.getAttribute('src')) {
3390          healElementorBackgroundVideo(box, native);
3391          return;
3392        }
3393      }
3394      if (
3395        box.getAttribute('data-vimeo-initialized') === 'true' &&
3396        box.querySelector('iframe.elementor-background-video-embed:not(.elementor-video-iframe)') &&
3397        owner.getAttribute('data-ucpf-video-hydrated') !== '1'
3398      ) {
3399        healElementorBackgroundVideo(box);
3400        return;
3401      }
3402
3403      // Live iframe already present (Elementor or restored) — heal, do not strip/reinject.
3404      if (healElementorBackgroundVideo(box)) {
3405        return;
3406      }
3407
3408      // Strip broken UCPF custom iframes; keep/restore presentation div.
3409      restoreElementorBackgroundShell(owner);
3410      box.removeAttribute('data-vimeo-initialized');
3411      run(owner);
3412
3413      // Elementor often will not re-bind background video after a failed first pass.
3414      // Fall back to markup that matches plugin-disabled Elementor output.
3415      window.setTimeout(function () {
3416        var existing = box.querySelector('iframe.elementor-background-video-embed:not(.elementor-video-iframe)');
3417        if (existing && existing.getAttribute('src')) {
3418          healElementorBackgroundVideo(box, existing);
3419          return;
3420        }
3421        whenPlayerApiReady(/vimeo/i.test(String((parseDataSettings(owner) || {}).background_video_link || '')), function () {
3422          injectElementorNativeBackground(owner);
3423        });
3424      }, 350);
3425    });
3426
3427    queryAll(['.elementor-widget-video', '[data-widget_type="video.default"]']).forEach(function (widget) {
3428      // Always restore parked Elementor frames first; never invent a sibling yet.
3429      restoreParkedVideoIframes(widget);
3430      dedupeElementorOpenInlineVideos(widget);
3431
3432      if (widget.getAttribute('data-ucpf-video-hydrated') === '1') {
3433        try {
3434          widget.querySelectorAll('iframe.elementor-video-iframe').forEach(function (iframe) {
3435            iframe.remove();
3436          });
3437          widget.removeAttribute('data-ucpf-video-hydrated');
3438        } catch (eW) { /* ignore */ }
3439      }
3440
3441      if (hasLiveVideoPlayer(widget)) {
3442        dedupeElementorOpenInlineVideos(widget);
3443        return;
3444      }
3445
3446      run(widget);
3447      // Restore-only passes while Elementor remounts.
3448      window.setTimeout(function () {
3449        restoreParkedVideoIframes(widget);
3450        injectElementorOpenInlineVideo(widget, { allowCreate: false });
3451        dedupeElementorOpenInlineVideos(widget);
3452      }, 200);
3453      // Late create only if Elementor left the open-inline shell empty.
3454      window.setTimeout(function () {
3455        if (!hasLiveVideoPlayer(widget)) {
3456          injectElementorOpenInlineVideo(widget, { allowCreate: true });
3457        }
3458        dedupeElementorOpenInlineVideos(widget);
3459      }, 1200);
3460    });
3461
3462    return hasEl;
3463  }
3464
3465  /**
3466   * Ensure the mount has sizing so the absolute iframe is visible.
3467   * @param {Element} mount
3468   * @param {Element} widget
3469   */
3470  function ensureVideoMountBox(mount, widget) {
3471    if (!mount) {
3472      return;
3473    }
3474    try {
3475      var cs = window.getComputedStyle(mount);
3476      if (cs.position === 'static') {
3477        mount.style.position = 'relative';
3478      }
3479      var h = mount.getBoundingClientRect().height;
3480      if (h < 40) {
3481        var wrap = widget && widget.querySelector ? widget.querySelector('.elementor-wrapper') : null;
3482        if (wrap && wrap.classList.contains('elementor-fit-aspect-ratio')) {
3483          /* Elementor aspect-ratio CSS should size it */
3484        } else if (!mount.style.paddingBottom && !mount.style.aspectRatio) {
3485          mount.style.aspectRatio = '16 / 9';
3486          mount.style.width = '100%';
3487          mount.style.minHeight = '200px';
3488        }
3489      }
3490    } catch (eBox) {
3491      /* ignore */
3492    }
3493  }
3494
3495  /**
3496   * Elementor / Divi / Gutenberg leave empty shells until their player JS runs.
3497   * After Marketing + Embeds consent, inject the iframe ourselves (Shorts-safe).
3498   */
3499  function hydrateBuilderVideos() {
3500    var allowYt = hasCategoryConsent('marketing') && hasCategoryConsent('functional');
3501    var allowVim = hasCategoryConsent('functional');
3502    if (!allowYt && !allowVim) {
3503      return 0;
3504    }
3505
3506    var count = 0;
3507
3508    function alreadyHasPlayer(root) {
3509      if (!root || !root.querySelector) {
3510        return true;
3511      }
3512      // Live or deferred (data-src) players — never clobber; loader restores data-src verbatim.
3513      return !!(
3514        root.querySelector(
3515          'iframe[src*="youtube"], iframe[src*="youtu.be"], iframe[src*="youtube-nocookie"], iframe[src*="vimeo"],' +
3516            'iframe[data-src*="youtube"], iframe[data-src*="youtu.be"], iframe[data-src*="youtube-nocookie"], iframe[data-src*="vimeo"]'
3517        ) || root.querySelector('video[src], video source[src]')
3518      );
3519    }
3520
3521    function resolveEmbedSrc(root, mount, built) {
3522      var exact = existingEmbedUrl(root) || existingEmbedUrl(mount);
3523      if (exact && /youtube|youtu\.be|vimeo/i.test(exact)) {
3524        return exact;
3525      }
3526      return built || '';
3527    }
3528
3529    function injectInto(mount, src, title, widget) {
3530      if (!mount || !src || mount.querySelector('iframe')) {
3531        return false;
3532      }
3533      ensureVideoMountBox(mount, widget);
3534      mount.appendChild(createConsentVideoIframe(src, title));
3535      mount.setAttribute('data-ucpf-video-hydrated', '1');
3536      return true;
3537    }
3538
3539    // Elementor Video widgets — never invent markup; Elementor sizes/styles natively.
3540    // (Handled in reinitElementorVideos / runReadyTrigger below.)
3541
3542    // Do NOT inject Elementor background videos here — that breaks Elementor's
3543    // cover sizing (width/height px). reinitElementorVideos restores the shell
3544    // and runs Elementor's own Vimeo/YouTube background handler.
3545
3546    // Gutenberg / Divi / WPBakery / Bricks empty shells with URL in attrs/classes.
3547    queryAll([
3548      '.wp-block-embed-youtube',
3549      '.wp-block-embed.is-provider-youtube',
3550      '.wp-block-embed-vimeo',
3551      '.wp-block-embed.is-provider-vimeo',
3552      '.et_pb_video',
3553      '.wpb_video_widget',
3554      '.fl-module-video',
3555      '.brxe-video',
3556    ]).forEach(function (shell) {
3557      if (alreadyHasPlayer(shell) || shell.getAttribute('data-ucpf-video-hydrated') === '1') {
3558        return;
3559      }
3560      // Skip if this shell is inside Elementor — Elementor owns those.
3561      if (shell.closest && shell.closest('.elementor-element, .elementor')) {
3562        return;
3563      }
3564      var blob =
3565        (shell.getAttribute('data-src') || '') +
3566        ' ' +
3567        (shell.getAttribute('data-url') || '') +
3568        ' ' +
3569        (shell.getAttribute('data-video-url') || '') +
3570        ' ' +
3571        (shell.innerHTML || '');
3572      var mount =
3573        shell.querySelector('.wp-block-embed__wrapper') ||
3574        shell.querySelector('.et_pb_video_box') ||
3575        shell.querySelector('.fluid-width-video-wrapper') ||
3576        shell;
3577      var src = '';
3578      var cls = (shell.className || '').toLowerCase();
3579      var built = '';
3580      if (/vimeo/.test(cls + blob)) {
3581        if (!allowVim) {
3582          return;
3583        }
3584        var vm = blob.match(/https?:\/\/[^\s"'<>]*vimeo\.com\/[^\s"'<>]+/i);
3585        built = vimeoEmbedSrc(vm ? vm[0] : blob);
3586      } else {
3587        if (!allowYt) {
3588          return;
3589        }
3590        var ym = blob.match(/https?:\/\/[^\s"'<>]*(?:youtube\.com|youtu\.be)\/[^\s"'<>]+/i);
3591        built = youtubeEmbedSrc(ym ? ym[0] : blob, {});
3592      }
3593      src = resolveEmbedSrc(shell, mount, built);
3594      if (injectInto(mount, src, 'Embedded video', shell)) {
3595        shell.setAttribute('data-ucpf-video-hydrated', '1');
3596        count += 1;
3597      }
3598    });
3599
3600    return count;
3601  }
3602
3603  function ensureVideosIfNeeded() {
3604    // Always hydrate videos after consent — leaveBuildersAlone only skips layout/Motion FX
3605    // recovery, never GDPR unlock for YouTube/Vimeo (same rule as maps).
3606    if (!hasCategoryConsent('marketing') || !hasCategoryConsent('functional')) {
3607      return;
3608    }
3609    watchElementorBackgroundVideos();
3610    var runRestore = function () {
3611      restoreAllParkedVideoIframes();
3612      dedupeElementorOpenInlineVideos(document);
3613    };
3614    var runHydrate = function (allowCreate) {
3615      restoreAllParkedVideoIframes();
3616      // Live BG iframes often exist before YT/Vimeo APIs — clear stuck loading immediately.
3617      healAllElementorBackgroundVideos();
3618      reinitElementorVideos();
3619      healAllElementorBackgroundVideos();
3620      hydrateBuilderVideos();
3621      queryAll(['.elementor-widget-video', '[data-widget_type="video.default"]']).forEach(function (widget) {
3622        injectElementorOpenInlineVideo(widget, { allowCreate: !!allowCreate });
3623        dedupeElementorOpenInlineVideos(widget);
3624      });
3625      dedupeElementorOpenInlineVideos(document);
3626    };
3627    // Immediate: put Elementor's parked URL back + heal stuck BG shells (no API wait).
3628    runRestore();
3629    healAllElementorBackgroundVideos();
3630    runHydrate(false);
3631    // Do not wait for Vimeo on YouTube-only pages (was whenPlayerApiReady(true) → 5s delay).
3632    whenPlayerApiReady(false, function () {
3633      runHydrate(false);
3634    });
3635    window.setTimeout(function () {
3636      runHydrate(false);
3637    }, 300);
3638    window.setTimeout(function () {
3639      runHydrate(false);
3640    }, 800);
3641    // Late fallback create only if Elementor never mounted a player.
3642    window.setTimeout(function () {
3643      runHydrate(true);
3644    }, 1400);
3645    window.setTimeout(function () {
3646      runHydrate(true);
3647      healAllElementorBackgroundVideos();
3648    }, 2500);
3649    window.setTimeout(healAllElementorBackgroundVideos, 4000);
3650  }
3651
3652  /**
3653   * After Marketing+Embeds: Smash Balloon feeds were parked. One-shot resize so
3654   * list/masonry layouts measure after the overlay is removed. Do not call
3655   * Smash Balloon internals or loop applyConsent (Mapster freeze).
3656   */
3657  var smashBalloonHydrated = false;
3658
3659  function ensureSmashBalloonIfNeeded() {
3660    if (smashBalloonHydrated) {
3661      return;
3662    }
3663    if (!hasCategoryConsent('marketing') || !hasCategoryConsent('functional')) {
3664      return;
3665    }
3666    var hasFeed = false;
3667    try {
3668      hasFeed = !!document.querySelector('.sb_youtube, .sb-wall, [id^="sb_youtube"], [id^="sb-wall"]');
3669    } catch (eQ) {
3670      hasFeed = false;
3671    }
3672    if (!hasFeed) {
3673      return;
3674    }
3675    smashBalloonHydrated = true;
3676    try {
3677      if (window.jQuery) {
3678        window.jQuery(window).trigger('resize');
3679      }
3680    } catch (eJq) {
3681      /* ignore */
3682    }
3683    try {
3684      window.dispatchEvent(new Event('resize'));
3685    } catch (eEv) {
3686      /* ignore */
3687    }
3688  }
3689
3690  function videoHostContainer(node) {
3691    if (!node) {
3692      return null;
3693    }
3694    // Prefer a sized shell so the overlay has layout (empty .elementor-video is fine).
3695    if (node.classList) {
3696      if (node.classList.contains('elementor-widget-video') || node.getAttribute('data-widget_type') === 'video.default') {
3697        return node;
3698      }
3699      if (
3700        isBackgroundVideoOwner(node) ||
3701        node.classList.contains('elementor-background-video-container') ||
3702        node.classList.contains('elementor-background-video-embed')
3703      ) {
3704        return resolveBackgroundVideoHost(node);
3705      }
3706      if (node.classList.contains('elementor-wrapper') || node.classList.contains('elementor-video')) {
3707        return node.closest('.elementor-widget-video') || node.closest('[data-widget_type="video.default"]') || node;
3708      }
3709      if (
3710        node.classList.contains('et_pb_video') ||
3711        node.classList.contains('wpb_video_widget') ||
3712        node.classList.contains('fl-module-video') ||
3713        node.classList.contains('brxe-video') ||
3714        node.classList.contains('wp-block-embed-youtube') ||
3715        node.classList.contains('wp-block-embed-vimeo') ||
3716        node.classList.contains('wp-block-video') ||
3717        node.classList.contains('wp-block-embed')
3718      ) {
3719        return node;
3720      }
3721    }
3722    if (node.tagName === 'IFRAME' && node.parentElement) {
3723      var parent = node.parentElement;
3724      if (parent.classList.contains('ucpf-consent-guard')) {
3725        return node;
3726      }
3727      // Prefer known media shells; otherwise guard the iframe itself (avoid wrapping huge parents).
3728      if (
3729        parent.classList.contains('elementor-wrapper') ||
3730        parent.classList.contains('elementor-video') ||
3731        parent.classList.contains('elementor-fit-aspect-ratio') ||
3732        parent.classList.contains('wp-block-embed__wrapper') ||
3733        parent.classList.contains('fluidvids') ||
3734        parent.classList.contains('et_pb_video_box') ||
3735        parent.classList.contains('fl-video')
3736      ) {
3737        return (
3738          parent.closest('.elementor-widget-video') ||
3739          parent.closest('[data-widget_type="video.default"]') ||
3740          parent.closest('.et_pb_video') ||
3741          parent.closest('.wp-block-embed') ||
3742          parent
3743        );
3744      }
3745      return node;
3746    }
3747    return node;
3748  }
3749
3750  /**
3751   * True for blocked library scripts / JS APIs — not playable embeds.
3752   *
3753   * @param {string} src
3754   * @return {boolean}
3755   */
3756  function isNonVisualMediaUrl(src) {
3757    src = String(src || '').toLowerCase();
3758    if (!src) {
3759      return false;
3760    }
3761    if (/\/api\/player\.js/.test(src)) {
3762      return true;
3763    }
3764    // Script assets ending in .js — keep player.vimeo.com/video/... allowed.
3765    if (/\.js(\?|#|$)/.test(src)) {
3766      return true;
3767    }
3768    return false;
3769  }
3770
3771  /**
3772   * Surface overlays only belong on visual embeds (iframe/map/shell), never on
3773   * blocked library scripts (e.g. GTM4WP player.vimeo.com/api/player.js).
3774   *
3775   * @param {Element} node
3776   * @return {boolean}
3777   */
3778  function isVisualEmbedSurface(node) {
3779    if (!node || !node.tagName) {
3780      return false;
3781    }
3782    if (isForbiddenGuardHost(node)) {
3783      return false;
3784    }
3785    var tag = node.tagName.toUpperCase();
3786    if (
3787      tag === 'SCRIPT' ||
3788      tag === 'LINK' ||
3789      tag === 'STYLE' ||
3790      tag === 'META' ||
3791      tag === 'NOSCRIPT' ||
3792      tag === 'TEMPLATE' ||
3793      tag === 'SOURCE' ||
3794      tag === 'BASE'
3795    ) {
3796      return false;
3797    }
3798    if (/gtm4wp/i.test(node.id || '') || /gtm4wp/i.test(node.className || '')) {
3799      return false;
3800    }
3801    var src =
3802      node.getAttribute('data-src') ||
3803      node.getAttribute('src') ||
3804      node.getAttribute('data-lazy-src') ||
3805      '';
3806    if (isNonVisualMediaUrl(src)) {
3807      return false;
3808    }
3809    return true;
3810  }
3811
3812  /** Tear down guards wrongly applied to scripts / empty non-visual wrappers. */
3813  function teardownNonVisualGuards() {
3814    teardownDocumentChromeGuards();
3815    // Heal any cover accidentally placed on the accessibility toolbar.
3816    try {
3817      queryAll([
3818        '#userwayAccessibilityIcon.ucpf-consent-guard',
3819        '.userway_buttons_wrapper.ucpf-consent-guard',
3820        '[id*="userway"].ucpf-consent-guard',
3821        '[class*="userway"].ucpf-consent-guard',
3822        '.uwy.ucpf-consent-guard',
3823      ]).forEach(function (el) {
3824        removeGuard(el);
3825      });
3826    } catch (eUwHeal) { /* ignore */ }
3827    // Un-park UserWay scripts left gated by older builds (Preferences / Embeds).
3828    try {
3829      queryAll([
3830        'script[data-src*="userway"]',
3831        'script[data-src*="cdn.userway.org"]',
3832        'script[type="text/plain"][data-src*="userway"]',
3833      ]).forEach(function (script) {
3834        var src = script.getAttribute('data-src') || '';
3835        if (!isUserWayUrl(src)) {
3836          return;
3837        }
3838        if (script.getAttribute('src')) {
3839          script.removeAttribute('data-ucpf-gated');
3840          script.removeAttribute('data-ucpf-category');
3841          script.removeAttribute('data-ucpf-service');
3842          return;
3843        }
3844        script.type = 'text/javascript';
3845        script.src = src;
3846        script.removeAttribute('data-src');
3847        script.removeAttribute('data-ucpf-gated');
3848        script.removeAttribute('data-ucpf-category');
3849        script.removeAttribute('data-ucpf-service');
3850      });
3851    } catch (eUwPark) { /* ignore */ }
3852    queryAll(['script[data-ucpf-consent-guarded="1"]']).forEach(function (script) {
3853      removeGuard(script);
3854    });
3855    // Migrate legacy overlays that decorated the Elementor video fill instead of the e-con owner.
3856    queryAll(['.elementor-background-video-container.ucpf-consent-guard']).forEach(function (box) {
3857      if (box.getAttribute('data-ucpf-consent-guarded') === '1') {
3858        removeGuard(box);
3859      }
3860    });
3861    // Older builds forced position:absolute !important + --embed on bg e-cons (broke layout).
3862    queryAll(['.ucpf-consent-guard--bg']).forEach(function (host) {
3863      host.classList.remove('ucpf-consent-guard--embed', 'ucpf-consent-guard--form');
3864      if (host.getAttribute('data-ucpf-bg-pos') === '1') {
3865        host.style.removeProperty('position');
3866        host.removeAttribute('data-ucpf-bg-pos');
3867      }
3868    });
3869    queryAll(['.ucpf-consent-guard--embed']).forEach(function (wrap) {
3870      // In-place shells (Elementor widget etc.) keep their own markup children — never tear those down here.
3871      if (wrap.getAttribute('data-ucpf-guard-shell') === '1') {
3872        return;
3873      }
3874      // Heal wraps that somehow contain <body>.
3875      for (var bi = 0; bi < wrap.children.length; bi++) {
3876        if (wrap.children[bi] === document.body || String(wrap.children[bi].tagName || '').toUpperCase() === 'BODY') {
3877          teardownDocumentChromeGuards();
3878          return;
3879        }
3880      }
3881      var kids = [];
3882      for (var i = 0; i < wrap.children.length; i++) {
3883        var child = wrap.children[i];
3884        if (!child.classList.contains('ucpf-consent-guard__panel')) {
3885          kids.push(child);
3886        }
3887      }
3888      if (kids.length === 1 && !isVisualEmbedSurface(kids[0])) {
3889        removeGuard(kids[0]);
3890      }
3891    });
3892  }
3893
3894  /**
3895   * Heal catastrophic wraps: consent guard that reparented <body> (breaks Elementor MO/sticky).
3896   */
3897  function teardownDocumentChromeGuards() {
3898    try {
3899      var body = document.body;
3900      if (body && body.parentElement && body.parentElement.classList && body.parentElement.classList.contains('ucpf-consent-guard')) {
3901        var wrap = body.parentElement;
3902        var grand = wrap.parentNode;
3903        if (grand) {
3904          while (wrap.firstChild) {
3905            var child = wrap.firstChild;
3906            if (child.classList && child.classList.contains('ucpf-consent-guard__panel')) {
3907              wrap.removeChild(child);
3908              continue;
3909            }
3910            grand.insertBefore(child, wrap);
3911          }
3912          if (wrap.parentNode === grand) {
3913            grand.removeChild(wrap);
3914          }
3915        }
3916      }
3917    } catch (eUnwrap) { /* ignore */ }
3918
3919    [document.documentElement, document.head, document.body].forEac
3919h(function (el) {
3920      if (!el || !el.classList) {
3921        return;
3922      }
3923      try {
3924        el.classList.remove(
3925          'ucpf-consent-guard',
3926          'ucpf-captcha-guard',
3927          'ucpf-consent-guard--active',
3928          'ucpf-consent-guard--embed',
3929          'ucpf-consent-guard--form',
3930          'ucpf-consent-guard--bg'
3931        );
3932        el.removeAttribute('data-ucpf-consent-guarded');
3933        el.removeAttribute('data-ucpf-guard-shell');
3934        el.removeAttribute('data-ucpf-guard-kind');
3935        el.removeAttribute('data-ucpf-guard-category');
3936        el.removeAttribute('data-ucpf-size-locked');
3937        el.removeAttribute('data-ucpf-guard-min-h');
3938        el.style.removeProperty('min-height');
3939        var panels = [];
3940        try {
3941          panels = el.querySelectorAll(':scope > .ucpf-consent-guard__panel');
3942        } catch (eScope) {
3943          for (var pi = 0; pi < el.children.length; pi++) {
3944            if (el.children[pi].classList && el.children[pi].classList.contains('ucpf-consent-guard__panel')) {
3945              panels.push(el.children[pi]);
3946            }
3947          }
3948        }
3949        Array.prototype.forEach.call(panels, function (p) {
3950          if (p && p.parentNode === el) {
3951            p.parentNode.removeChild(p);
3952          }
3953        });
3954      } catch (eClean) { /* ignore */ }
3955    });
3956  }
3957
3958  /**
3959   * True when a form contains any captcha / security widget signal
3960   * (covers custom themes that invent class names).
3961   *
3962   * @param {Element} form
3963   * @return {boolean}
3964   */
3965  function formHasCaptchaSignal(form) {
3966    if (!form || !form.querySelector) {
3967      return false;
3968    }
3969    var i;
3970    for (i = 0; i < CAPTCHA_MARKERS.length; i++) {
3971      try {
3972        if (form.querySelector(CAPTCHA_MARKERS[i])) {
3973          return true;
3974        }
3975      } catch (e1) {
3976        /* invalid selector */
3977      }
3978    }
3979    for (i = 0; i < CAPTCHA_FORM_INNER_SELECTORS.length; i++) {
3980      try {
3981        if (form.querySelector(CAPTCHA_FORM_INNER_SELECTORS[i])) {
3982          return true;
3983        }
3984      } catch (e2) {
3985        /* invalid selector */
3986      }
3987    }
3988    // Name / URL heuristics for unknown markup.
3989    var named = form.querySelectorAll('[name], iframe[src], iframe[data-src], script[src], script[data-src]');
3990    for (i = 0; i < named.length; i++) {
3991      var el = named[i];
3992      var blob = (
3993        (el.getAttribute('name') || '') +
3994        ' ' +
3995        (el.getAttribute('src') || '') +
3996        ' ' +
3997        (el.getAttribute('data-src') || '') +
3998        ' ' +
3999        (el.getAttribute('data-ucpf-category') || '')
4000      ).toLowerCase();
4001      if (/(recaptcha|h-captcha|hcaptcha|cf-turnstile|turnstile|frc-|friendlycaptcha)/.test(blob)) {
4002        return true;
4003      }
4004      if (
4005        el.getAttribute('data-ucpf-category') === 'security' &&
4006        /(recaptcha|hcaptcha|turnstile|friendlycaptcha|challenges\.cloudflare)/.test(blob)
4007      ) {
4008        return true;
4009      }
4010    }
4011    return false;
4012  }
4013
4014  function collectTargets() {
4015    /** @type {{ target: Element, kind: string, category: string, categories: string[], mode: string }[]} */
4016    var items = [];
4017    var seen = [];
4018
4019    function push(target, kind, category, mode, categories) {
4020      if (!target || isForbiddenGuardHost(target) || isUserWayNode(target)) {
4021        return;
4022      }
4023      if (mode === 'embed' && !isVisualEmbedSurface(target)) {
4024        return;
4025      }
4026      if (mode === 'embed' && isEffectivelyHidden(target) && !isSmashBalloonFeedHost(target)) {
4027        return;
4028      }
4029      for (var i = 0; i < seen.length; i++) {
4030        if (seen[i] === target) {
4031          return;
4032        }
4033        // Maps: one cover on the outermost host — skip nested markers already covered,
4034        // or replace an inner host when a better outer host is collected later.
4035        if (kind === 'map' && seen[i] && seen[i].nodeType === 1 && target.nodeType === 1) {
4036          try {
4037            if (seen[i].contains && seen[i].contains(target)) {
4038              return;
4039            }
4040            if (target.contains && target.contains(seen[i])) {
4041              var inner = seen[i];
4042              seen.splice(i, 1);
4043              for (var j = items.length - 1; j >= 0; j--) {
4044                if (items[j].target === inner) {
4045                  items.splice(j, 1);
4046                }
4047              }
4048              i--;
4049            }
4050          } catch (eNest) { /* ignore */ }
4051        }
4052      }
4053      seen.push(target);
4054      var cats = Array.isArray(categories) && categories.length ? categories : category ? [category] : [];
4055      items.push({
4056        target: target,
4057        kind: kind,
4058        category: category || cats[0] || '',
4059        categories: cats,
4060        mode: mode,
4061      });
4062    }
4063
4064    // WooCommerce checkout first — one combined Security + Embeds panel (no stacked overlays).
4065    queryAll(CHECKOUT_SELECTORS).forEach(function (node) {
4066      var host = node;
4067      if (node.tagName !== 'FORM') {
4068        host = (node.querySelector && (node.querySelector('form.checkout') || node.querySelector('form.woocommerce-checkout'))) || node;
4069      }
4070      push(host, 'checkout', 'functional', 'form', ['security', 'functional']);
4071    });
4072
4073    // CAPTCHA-backed forms → Security (skip checkout hosts — already covered above).
4074    // Always cover builder-hosted forms (Elementor etc.) — leaveBuildersAlone must
4075    // never suppress GDPR surface overlays.
4076    queryAll(CAPTCHA_MARKERS).forEach(function (node) {
4077      var form = findFormForNode(node);
4078      if (isCheckoutSurface(form) || isCheckoutSurface(node)) {
4079        return;
4080      }
4081      push(form, 'captcha', 'security', 'form', ['security']);
4082    });
4083
4084    // Any <form> with captcha-ish descendants (custom themes / unknown plugins).
4085    Array.prototype.forEach.call(document.querySelectorAll('form'), function (form) {
4086      if (isCheckoutSurface(form)) {
4087        return;
4088      }
4089      if (formHasCaptchaSignal(form)) {
4090        push(form, 'captcha', 'security', 'form', ['security']);
4091      }
4092    });
4093
4094    // Gravity Forms wrappers (AJAX / Elementor shortcode) — cover even if markers sit oddly.
4095    queryAll(['.gform_wrapper', 'form[id^="gform_"]']).forEach(function (node) {
4096      if (isCheckoutSurface(node)) {
4097        return;
4098      }
4099      var form =
4100        node.tagName === 'FORM'
4101          ? node
4102          : (node.querySelector && (node.querySelector('form[id^="gform_"]') || node.querySelector('form'))) || node;
4103      if (
4104        formHasCaptchaSignal(form) ||
4105        (form.querySelector &&
4106          form.querySelector(
4107            '.gfield--type-captcha, .ginput_recaptcha, .gform_recaptcha, .gform_captcha, [data-sitekey]'
4108          ))
4109      ) {
4110        push(form, 'captcha', 'security', 'form', ['security']);
4111      }
4112    });
4113
4114    // Catalog / blocker placeholders + network-gate parked iframes.
4115    queryAll([
4116      '.ucpf-iframe-placeholder[data-ucpf-category]',
4117      'iframe[data-ucpf-category][data-src]',
4118      'iframe[data-ucpf-gated="1"]',
4119      'iframe[data-ucpf-category]',
4120    ]).forEach(function (node) {
4121      var category = node.getAttribute('data-ucpf-category') || 'marketing';
4122      var kind = kindForPlaceholder(node);
4123      if (kind === 'embed' && category === 'functional') {
4124        kind = 'widget';
4125      }
4126      var cats =
4127        kind === 'youtube' || kind === 'vimeo' || kind === 'widget' || kind === 'calendly' || kind === 'map' || kind === 'embed'
4128          ? ensureEmbedConsentCategories([category])
4129          : [category];
4130      var host = node;
4131      if (node.tagName === 'IFRAME' && node.parentElement) {
4132        host = widgetEmbedHostContainer(node) || node.parentElement;
4133      }
4134      push(host, kind, category, 'embed', cats);
4135    });
4136
4137    // Live map widgets / iframes not yet replaced.
4138    queryAll(MAP_MARKERS).forEach(function (node) {
4139      var host = resolveMapHost(node);
4140      if (!host) {
4141        return;
4142      }
4143      push(host, 'map', 'functional', 'embed', ensureEmbedConsentCategories(['functional']));
4144    });
4145
4146    // Calendly inline widgets / iframes (Elementor popups inject these late).
4147    queryAll(CALENDLY_MARKERS).forEach(function (node) {
4148      if (isEffectivelyHidden(node)) {
4149        return;
4150      }
4151      var host = calendlyHostContainer(node);
4152      if (host) {
4153        push(host, 'calendly', 'functional', 'embed', ensureEmbedConsentCategories(['functional']));
4154      }
4155    });
4156
4157    // Amelia Booking — first-party SPA (Gravity Forms model): Security overlay only
4158    // when captcha is configured. Never treat as a third-party embed / don't park scripts.
4159    queryAll(AMELIA_FORM_MARKERS).forEach(function (node) {
4160      if (isEffectivelyHidden(node)) {
4161        return;
4162      }
4163      var host = node.id === 'amelia-container' ? node : (node.closest && node.closest('#amelia-container')) || node;
4164      // Always cover booking until Security — Amelia loads reCAPTCHA with the form (HAR).
4165      push(host, 'captcha', 'security', 'form', ['security']);
4166    });
4167
4168    // Jobber / Typeform / similar booking & form embeds.
4169    queryAll(WIDGET_EMBED_MARKERS).forEach(function (node) {
4170      if (isEffectivelyHidden(node)) {
4171        return;
4172      }
4173      var host = widgetEmbedHostContainer(node);
4174      if (host) {
4175        push(host, 'widget', 'functional', 'embed', ensureEmbedConsentCategories(['functional']));
4176      }
4177    });
4178
4179    // Gated embed scripts (Jobber CloudFront snippet, etc.) — cover the host box
4180    // even when the iframe has not been injected yet.
4181    queryAll(['script[data-ucpf-gated="1"]', 'script[data-ucpf-category][data-src]', 'script[type="text/plain"][data-src]']).forEac
4181h(
4182      function (script) {
4183        if (isUserWayNode(script) || isUserWayUrl(script.getAttribute('data-src') || script.getAttribute('src') || '')) {
4184          return;
4185        }
4186        if (!isEmbedLikeScript(script)) {
4187          return;
4188        }
4189        var host = widgetEmbedHostContainer(script);
4190        if (!host || isEffectivelyHidden(host) || isUserWayNode(host)) {
4191          return;
4192        }
4193        var category = script.getAttribute('data-ucpf-category') || 'functional';
4194        if (category !== 'functional' && category !== 'marketing' && category !== 'analytics') {
4195          category = 'functional';
4196        }
4197        push(host, 'widget', category, 'embed', ensureEmbedConsentCategories([category]));
4198      }
4199    );
4200    // Smash Balloon YouTube Feed — cover the feed root, never the spinner icon.
4201    queryAll(YOUTUBE_FEED_MARKERS).forEach(function (node) {
4202      if (isYoutubeFeedLoader(node)) {
4203        return;
4204      }
4205      var host = youtubeFeedHostContainer(node);
4206      if (host && !isYoutubeFeedLoader(host)) {
4207        push(host, 'youtube', 'marketing', 'embed', ensureVideoConsentCategories(['marketing']));
4208      }
4209    });
4210
4211    // Smash Balloon Social Wall (Instagram CDN / lightbox) — Marketing + Embeds.
4212    queryAll(SOCIAL_WALL_MARKERS).forEach(function (node) {
4213      var host = socialWallHostContainer(node);
4214      if (host) {
4215        push(host, 'embed', 'marketing', 'embed', ensureEmbedConsentCategories(['marketing']));
4216      }
4217    });
4218
4219    // YouTube / Vimeo always — including Elementor when leaveBuildersAlone is on.
4220    // Layout skips stay elsewhere; video covers must still run before consent.
4221    // Same-origin / Elementor Self Hosted media must never be covered.
4222    queryAll(VIDEO_MARKERS).forEach(function (node) {
4223      if (!isVisualEmbedSurface(node)) {
4224        return;
4225      }
4226      if (node.closest && (node.closest('.sb_youtube') || node.closest('.sb-wall'))) {
4227        return;
4228      }
4229      if (isSelfHostedVideoSurface(node)) {
4230        return;
4231      }
4232      var detected = detectVideoProvider(node);
4233      if (!detected) {
4234        var src = (node.getAttribute('src') || node.getAttribute('data-src') || '').toLowerCase();
4235        if (src && isSameOriginMediaUrl(src)) {
4236          return;
4237        }
4238        if (src.indexOf('vimeo') !== -1) {
4239          detected = { kind: 'vimeo', category: 'functional' };
4240        } else if (src && isVideoPlayerUrl(src)) {
4241          detected = { kind: 'youtube', category: 'marketing' };
4242        }
4243      }
4244      if (detected) {
4245        var host = videoHostContainer(node);
4246        if (host && isSelfHostedVideoSurface(host)) {
4247          return;
4248        }
4249        if (host && isVisualEmbedSurface(host)) {
4250          push(host, detected.kind, detected.category, 'embed', ensureVideoConsentCategories([detected.category]));
4251        }
4252      }
4253    });
4254
4255    // Builder shells (Elementor/Divi/Gutenberg/etc.) — iframe often injected later.
4256    queryAll(VIDEO_SHELL_SELECTORS).forEach(function (node) {
4257      if (isEffectivelyHidden(node)) {
4258        return;
4259      }
4260      if (isSelfHostedVideoSurface(node)) {
4261        return;
4262      }
4263      var detected = detectVideoProvider(node);
4264      if (!detected) {
4265        var cls = (node.className || '').toLowerCase();
4266        if (cls.indexOf('vimeo') !== -1) {
4267          detected = { kind: 'vimeo', category: 'functional' };
4268        } else if (cls.indexOf('youtube') !== -1) {
4269          detected = { kind: 'youtube', category: 'marketing' };
4270        }
4271      }
4272      if (!detected && (node.classList.contains('elementor-widget-video') || node.getAttribute('data-widget_type') === 'video.default')) {
4273        // Never invent YouTube for Self Hosted / same-origin Elementor videos.
4274        if (isSelfHostedVideoSurface(node)) {
4275          return;
4276        }
4277        var s = parseDataSettings(node);
4278        var vt = s && String(s.video_type || '').toLowerCase();
4279        if (vt === 'hosted' || vt === 'self' || vt === 'self_hosted' || vt === 'file') {
4280          return;
4281        }
4282        if (vt === 'vimeo' || (s && s.vimeo_url)) {
4283          detected = { kind: 'vimeo', category: 'functional' };
4284        } else if (vt === 'youtube' || (s && (s.youtube_url || s.youtube_id))) {
4285          detected = { kind: 'youtube', category: 'marketing' };
4286        } else {
4287          detected = detectVideoProvider(node);
4288        }
4289      }
4290      if (!detected && isBackgroundVideoShell(node)) {
4291        detected = detectVideoProvider(node);
4292      }
4293      if (detected) {
4294        var host = videoHostContainer(node);
4295        if (isBackgroundVideoShell(node) || (host && isBackgroundVideoShell(host))) {
4296          host = resolveBackgroundVideoHost(node);
4297        }
4298        if (host && isSelfHostedVideoSurface(host)) {
4299          return;
4300        }
4301        push(host, detected.kind, detected.category, 'embed', ensureVideoConsentCategories([detected.category]));
4302      }
4303    });
4304
4305    queryAll(['[data-settings*="background_video_link"]']).forEach(function (node) {
4306      if (isEffectivelyHidden(node)) {
4307        return;
4308      }
4309      if (isSelfHostedVideoSurface(node)) {
4310        return;
4311      }
4312      var box = node.querySelector('.elementor-background-video-container');
4313      if (box && isEffectivelyHidden(box)) {
4314        return;
4315      }
4316      var detected = detectVideoProvider(node) || (box ? detectVideoProvider(box) : null);
4317      if (detected) {
4318        push(
4319          resolveBackgroundVideoHost(node),
4320          detected.kind,
4321          detected.category,
4322          'embed',
4323          ensureVideoConsentCategories([detected.category])
4324        );
4325      }
4326    });
4327
4328    return items;
4329  }
4330
4331  function activateLoaderFallback() {
4332    if (window.UCPFLoader && typeof window.UCPFLoader.applyConsent === 'function' && window.UCPF) {
4333      try {
4334        window.UCPFLoader.applyConsent(window.UCPF.getConsent());
4335      } catch (e) {
4336        /* ignore */
4337      }
4338    }
4339  }
4340
4341  var refreshBusy = false;
4342  function refresh() {
4343    if (window.__ucpfDiscover || refreshBusy) {
4344      return;
4345    }
4346    refreshBusy = true;
4347    try {
4348      teardownNonVisualGuards();
4349      var items = collectTargets();
4350      items.forEach(function (item) {
4351        var cats = item.categories && item.categories.length ? item.categories : [item.category];
4352        if (hasAllCategories(cats)) {
4353          removeGuard(item.target);
4354        } else {
4355          applyGuard(item.target, item.kind, item.category, item.mode, cats);
4356        }
4357      });
4358    } finally {
4359      refreshBusy = false;
4360    }
4361  }
4362
4363  function ensureCalendlyIfNeeded() {
4364    if (!hasCategoryConsent('functional')) {
4365      return;
4366    }
4367    var nodes = document.querySelectorAll('.calendly-inline-widget[data-url], .calendly-badge-widget[data-url]');
4368    if (!nodes.length) {
4369      return;
4370    }
4371    var needs = false;
4372    Array.prototype.forEach.call(nodes, function (el) {
4373      if (!el.querySelector || !el.querySelector('iframe')) {
4374        needs = true;
4375      }
4376    });
4377    if (needs) {
4378      reinitCalendlyWidgets();
4379    }
4380  }
4381
4382  /**
4383   * After Security consent: captcha APIs may load after GF already tried once.
4384   * Re-render empty widgets and nudge Gravity Forms (same idea as Calendly).
4385   */
4386  function captchaWidgetNeedsRender(el) {
4387    if (!el) {
4388      return false;
4389    }
4390    var hasLiveFrame = !!(
4391      el.querySelector &&
4392      el.querySelector(
4393        'iframe[src*="recaptcha"], iframe[src*="hcaptcha"], iframe[src*="turnstile"], iframe[title*="reCAPTCHA"], iframe[title*="hCaptcha"]'
4394      )
4395    );
4396    var hasResponseBox = !!(
4397      el.querySelector &&
4398      el.querySelector('textarea.g-recaptcha-response, textarea[name="g-recaptcha-response"], textarea[name="h-captcha-response"], [name="cf-turnstile-response"]')
4399    );
4400    // Gravity Forms: already bound (see .ginput_recaptcha[data-widget-id] + gform-initialized).
4401    if (el.getAttribute('data-widget-id') != null && String(el.getAttribute('data-widget-id')) !== '') {
4402      if (hasLiveFrame || hasResponseBox) {
4403        return false;
4404      }
4405      // Stale widget-id with no live frame — clear so GF can bind again (new tab / bfcache).
4406      try {
4407        el.removeAttribute('data-widget-id');
4408      } catch (eWid) { /* ignore */ }
4409      if (el.classList) {
4410        el.classList.remove('gform-initialized');
4411      }
4412      return true;
4413    }
4414    if (el.classList && el.classList.contains('gform-initialized') && (hasLiveFrame || hasResponseBox)) {
4415      return false;
4416    }
4417    if (hasLiveFrame) {
4418      return false;
4419    }
4420    if (el.querySelector && el.querySelector('iframe') && hasResponseBox) {
4421      return false;
4422    }
4423    // Invisible widgets may have no iframe yet — still need an API render pass.
4424    return true;
4425  }
4426
4427  /** True when a captcha host on the page still looks unbound / empty. */
4428  function captchaSurfacesNeedHelp() {
4429    var nodes = document.querySelectorAll(
4430      '.g-recaptcha, .h-captcha, .cf-turnstile, .ginput_recaptcha, .gform_recaptcha'
4431    );
4432    if (!nodes.length) {
4433      return false;
4434    }
4435    for (var i = 0; i < nodes.length; i++) {
4436      if (captchaWidgetNeedsRender(nodes[i])) {
4437        return true;
4438      }
4439    }
4440    return false;
4441  }
4442
4443  function nudgeGravityFormsCaptcha() {
4444    try {
4445      if (!window.jQuery) {
4446        return;
4447      }
4448      var $ = window.jQuery;
4449      // Clear stale GF init flags on empty captcha hosts so post_render rebinds.
4450      $('.ginput_recaptcha, .gform_recaptcha').each(function () {
4451        var el = this;
4452        if (captchaWidgetNeedsRender(el) && el.classList) {
4453          el.classList.remove('gform-initialized');
4454        }
4455      });
4456      $('.gform_wrapper').each(function () {
4457        var wrap = this;
4458        var formId = 0;
4459        try {
4460          var m = (wrap.id || '').match(/gform_wrapper_(\d+)/);
4461          if (m) {
4462            formId = parseInt(m[1], 10) || 0;
4463          }
4464        } catch (eId) {
4465          formId = 0;
4466        }
4467        try {
4468          $(document).trigger('gform_post_render', [formId, 1]);
4469        } catch (eTrig) {
4470          /* ignore */
4471        }
4472      });
4473    } catch (eGf) {
4474      /* ignore */
4475    }
4476  }
4477
4478  var captchaReinitScheduled = false;
4479
4480  function reinitCaptchaWidgets() {
4481    if (!hasCategoryConsent('security')) {
4482      return;
4483    }
4484    if (captchaReinitScheduled) {
4485      return;
4486    }
4487    captchaReinitScheduled = true;
4488    if (window.UCPFLoader && typeof window.UCPFLoader.applyConsent === 'function') {
4489      try {
4490        window.UCPFLoader.applyConsent();
4491      } catch (eLoad) {
4492        /* ignore */
4493      }
4494    }
4495    var attempt = 0;
4496    function tryInit() {
4497      attempt += 1;
4498
4499      try {
4500        var gr = window.grecaptcha;
4501        if (gr && typeof gr.render === 'function') {
4502          var ready = typeof gr.ready === 'function' ? gr.ready.bind(gr) : function (fn) {
4503            fn();
4504          };
4505          ready(function () {
4506            // Generic .g-recaptcha only — GF .ginput_recaptcha is owned by gform_post_render.
4507            Array.prototype.forEach.call(document.querySelectorAll('.g-recaptcha'), function (el) {
4508              if (!captchaWidgetNeedsRender(el)) {
4509                return;
4510              }
4511              if (el.getAttribute('data-ucpf-captcha-rendered') === '1') {
4512                return;
4513              }
4514              try {
4515                gr.render(el);
4516                el.setAttribute('data-ucpf-captcha-rendered', '1');
4517              } catch (eRender) {
4518                /* already rendered or missing sitekey — leave for GF nudge */
4519              }
4520            });
4521          });
4522        }
4523      } catch (eGr) {
4524        /* ignore */
4525      }
4526
4527      try {
4528        var hc = window.hcaptcha;
4529        if (hc && typeof hc.render === 'function') {
4530          Array.prototype.forEach.call(document.querySelectorAll('.h-captcha'), function (el) {
4531            if (!captchaWidgetNeedsRender(el)) {
4532              return;
4533            }
4534            if (el.getAttribute('data-ucpf-captcha-rendered') === '1') {
4535              return;
4536            }
4537            try {
4538              hc.render(el);
4539              el.setAttribute('data-ucpf-captcha-rendered', '1');
4540            } catch (eHc) {
4541              /* ignore */
4542            }
4543          });
4544        }
4545      } catch (eH) {
4546        /* ignore */
4547      }
4548
4549      try {
4550        var ts = window.turnstile;
4551        if (ts && typeof ts.render === 'function') {
4552          Array.prototype.forEach.call(document.querySelectorAll('.cf-turnstile'), function (el) {
4553            if (!captchaWidgetNeedsRender(el)) {
4554              return;
4555            }
4556            if (el.getAttribute('data-ucpf-captcha-rendered') === '1') {
4557              return;
4558            }
4559            try {
4560              ts.render(el);
4561              el.setAttribute('data-ucpf-captcha-rendered', '1');
4562            } catch (eTs) {
4563              /* ignore */
4564            }
4565          });
4566        }
4567      } catch (eT) {
4568        /* ignore */
4569      }
4570
4571      // Always nudge GF while surfaces still look empty (API present ≠ widget live).
4572      if (window.grecaptcha || window.hcaptcha || window.turnstile || attempt > 2) {
4573        nudgeGravityFormsCaptcha();
4574      }
4575
4576      var stillNeed = captchaSurfacesNeedHelp();
4577      if (!stillNeed || attempt >= 24) {
4578        captchaReinitScheduled = false;
4579        return;
4580      }
4581      window.setTimeout(tryInit, 250);
4582    }
4583    tryInit();
4584  }
4585
4586  function ensureCaptchasIfNeeded() {
4587    if (!hasCategoryConsent('security')) {
4588      return;
4589    }
4590    var nodes = document.querySelectorAll(
4591      '.g-recaptcha, .h-captcha, .cf-turnstile, .ginput_recaptcha, .gform_recaptcha, .gform_wrapper .gfield--type-captcha, .gfield--type-captcha, form[id^="gform_"] [data-sitekey]'
4592    );
4593    if (!nodes.length) {
4594      return;
4595    }
4596    if (!captchaSurfacesNeedHelp()) {
4597      return;
4598    }
4599    reinitCaptchaWidgets();
4600  }
4601
4602  /**
4603   * After Marketing + Embeds: restore parked map iframes once and nudge Elementor.
4604   * Never cache-bust / reload live iframes in a loop — that spam-refreshes embeds.
4605   */
4606  var mapsHydrateStarted = false;
4607  var mapsHydrated = false;
4608  var mapsHydrateRetryScheduled = false;
4609  var animationsHydrateStarted = false;
4610
4611  function ensureAnimationsIfNeeded() {
4612    // GSAP CDN is never gated — only heal leftover parked tags from older builds.
4613    if (animationsHydrateStarted) {
4614      return;
4615    }
4616    var hasParkedAnim = !!document.querySelector(
4617      'script[type="text/plain"][data-src*="gsap"], script[data-ucpf-gated="1"][data-src*="gsap"], script[type="text/plain"][data-src*="lottie"], script[type="text/plain"][data-src*="dotlottie"], script[type="text/plain"][data-ucpf-inline-animation="1"]'
4618    );
4619    if (!hasParkedAnim) {
4620      return;
4621    }
4622    animationsHydrateStarted = true;
4623    if (window.UCPFLoader && typeof window.UCPFLoader.refireAnimationDependents === 'function') {
4624      try {
4625        window.UCPFLoader.refireAnimationDependents();
4626      } catch (eAnim) { /* ignore */ }
4627    }
4628  }
4629
4630  function isPlaceholderEmbedSrc(url) {
4631    var u = String(url || '').trim();
4632    if (!u || u === 'about:blank') {
4633      return true;
4634    }
4635    var lower = u.toLowerCase();
4636    return lower.indexOf('data:') === 0 || lower.indexOf('blob:') === 0 || lower.indexOf('javascript:') === 0;
4637  }
4638
4639  function resolveDeferredIframeUrl(iframe) {
4640    if (!iframe || iframe.tagName !== 'IFRAME') {
4641      return '';
4642    }
4643    if (typeof window.__ucpfResolveDeferredEmbedUrl === 'function') {
4644      return window.__ucpfResolveDeferredEmbedUrl(iframe);
4645    }
4646    var parked = iframe.getAttribute('data-src') || iframe.getAttribute('data-lazy-src') || '';
4647    var live = iframe.getAttribute('src') || '';
4648    if (parked && !isPlaceholderEmbedSrc(parked)) {
4649      if (isPlaceholderEmbedSrc(live)) {
4650        return parked;
4651      }
4652    }
4653    if (live && !isPlaceholderEmbedSrc(live)) {
4654      return live;
4655    }
4656    return parked || live || '';
4657  }
4658
4659  function restoreParkedMapIframe(iframe) {
4660    if (!iframe || iframe.tagName !== 'IFRAME') {
4661      return false;
4662    }
4663    if (iframe.getAttribute('data-ucpf-map-restored') === '1') {
4664      return false;
4665    }
4666    var parked = iframe.getAttribute('data-src') || '';
4667    var live = iframe.getAttribute('src') || '';
4668    var gated = iframe.getAttribute('data-ucpf-gated') === '1';
4669    var blank = !live || live === 'about:blank' || isPlaceholderEmbedSrc(live);
4670    // Only restore when gated/blank — do not touch a healthy live maps iframe.
4671    if (!gated && !blank) {
4672      if (isMapEmbedSrc(live)) {
4673        iframe.setAttribute('data-ucpf-map-restored', '1');
4674      }
4675      return false;
4676    }
4677    var src = resolveDeferredIframeUrl(iframe);
4678    if (!isMapEmbedSrc(src)) {
4679      return false;
4680    }
4681    // Strip any leftover cache-bust from older builds.
4682    src = String(src).replace(/([?&])ucpf_r=\d+/g, '$1').replace(/[?&]$/, '');
4683    try {
4684      iframe.src = src;
4685    } catch (eSet) {
4686      return false;
4687    }
4688    iframe.setAttribute('data-ucpf-map-restored', '1');
4689    iframe.removeAttribute('data-src');
4690    iframe.removeAttribute('data-ucpf-gated');
4691    iframe.removeAttribute('data-ucpf-category');
4692    iframe.removeAttribute('data-ucpf-parked');
4693    return true;
4694  }
4695
4696  function isMapEmbedSrc(src) {
4697    var u = String(src || '').toLowerCase();
4698    return !!(
4699      u &&
4700      u !== 'about:blank' &&
4701      !isPlaceholderEmbedSrc(u) &&
4702      (/maps\.google|google\.com\/maps|mapbox\.com|openstreetmap\.org|maplibre|bing\.com\/maps|virtualearth|mapme\.com|maphub\.net|batchgeo\.com|zeemaps\.com|umap\.openstreetmap\.fr|arcgis\.com/i.test(
4703        u
4704      ))
4705    );
4706  }
4707
4708  function mapIframeIsLive(iframe) {
4709    if (!iframe || iframe.tagName !== 'IFRAME') {
4710      return false;
4711    }
4712    if (iframe.getAttribute('data-ucpf-gated') === '1') {
4713      return false;
4714    }
4715    var live = resolveDeferredIframeUrl(iframe);
4716    return isMapEmbedSrc(live);
4717  }
4718
4719  function reinitMapWidgets() {
4720    if (!hasCategoryConsent('marketing') || !hasCategoryConsent('functional')) {
4721      return;
4722    }
4723    if (mapsHydrated) {
4724      return;
4725    }
4726    if (window.UCPFLoader && typeof window.UCPFLoader.applyConsent === 'function') {
4727      try {
4728        window.UCPFLoader.applyConsent();
4729      } catch (eLoad) { /* ignore */ }
4730    }
4731
4732    queryAll(MAP_MARKERS).forEach(function (node) {
4733      if (node.querySelectorAll) {
4734        node.querySelectorAll('iframe').forEach(restoreParkedMapIframe);
4735      }
4736      if (node.tagName === 'IFRAME') {
4737        restoreParkedMapIframe(node);
4738      }
4739    });
4740    document
4741      .querySelectorAll(
4742        'iframe[data-ucpf-gated="1"][data-src*="maps.google"], iframe[data-ucpf-gated="1"][data-src*="google.com/maps"], iframe[data-ucpf-gated="1"][data-src*="mapme.com"], iframe[data-ucpf-gated="1"][data-src*="maphub.net"], iframe[data-ucpf-gated="1"][data-src*="batchgeo.com"], iframe[data-ucpf-gated="1"][data-src*="zeemaps.com"], iframe[data-ucpf-gated="1"][data-src*="umap.openstreetmap.fr"], iframe[data-ucpf-gated="1"][data-src*="arcgis.com"], iframe[data-ucpf-gated="1"][data-src*="bing.com/maps"]'
4743      )
4744      .forEach(restoreParkedMapIframe);
4745
4746    var hasEl =
4747      !!(
4748        window.elementorFrontend &&
4749        elementorFrontend.elementsHandler &&
4750        typeof elementorFrontend.elementsHandler.runReadyTrigger === 'function'
4751      );
4752    if (hasEl) {
4753      queryAll([
4754        '.elementor-widget-google_maps',
4755        '[data-widget_type="google_maps.default"]',
4756        '[data-widget_type*="google_maps"]',
4757      ]).forEach(function (widget) {
4758        try {
4759          widget.querySelectorAll('iframe').forEach(restoreParkedMapIframe);
4760          // Only re-boot Elementor when the map iframe is still missing/gated.
4761          var live = false;
4762          widget.querySelectorAll('iframe').forEach(function (iframe) {
4763            if (mapIframeIsLive(iframe)) {
4764              live = true;
4765            }
4766          });
4767          if (!live) {
4768            elementorFrontend.elementsHandler.runReadyTrigger(widget);
4769          }
4770        } catch (eRun) { /* ignore */ }
4771      });
4772    }
4773
4774    try {
4775      if (typeof window.initMap === 'function') {
4776        window.initMap();
4777      }
4778    } catch (eInit) { /* ignore */ }
4779    try {
4780      if (window.WPGMZA && typeof window.WPGMZA.maps !== 'undefined' && window.jQuery) {
4781        window.jQuery('.wpgmza_map').each(function () {
4782          try {
4783            window.jQuery(this).trigger('init');
4784          } catch (eOne) { /* ignore */ }
4785        });
4786      }
4787    } catch (eWpg) { /* ignore */ }
4788    try {
4789      // Mapster / MapLibre / Mapbox: force-refire plugin bootstrap at most once per page.
4790      // Skip when a live canvas already exists (avoid nested MapLibre after soft+force clone).
4791      if (
4792        !window.__ucpfMapsterForceDone &&
4793        !mapsterMapIsLive() &&
4794        window.UCPFLoader &&
4795        typeof window.UCPFLoader.refireMapDependents === 'function'
4796      ) {
4797        window.__ucpfMapsterForceDone = true;
4798        window.UCPFLoader.refireMapDependents({ forceMapster: true });
4799      } else if (mapsterMapIsLive()) {
4800        window.__ucpfMapsterForceDone = true;
4801      }
4802    } catch (eMapster) { /* ignore */ }
4803    try {
4804      if (window.jQuery) {
4805        window.jQuery(document).trigger('google.maps.loaded');
4806        window.jQuery(window).trigger('resize');
4807      }
4808    } catch (eJq) { /* ignore */ }
4809    try {
4810      window.dispatchEvent(new CustomEvent('ucpf:maps:ready'));
4811    } catch (eEv) { /* ignore */ }
4812
4813    var mapsterLive = mapsterMapIsLive();
4814    var canvasLive = !!document.querySelector(
4815      '.leaflet-container canvas, .mapboxgl-canvas, .maplibregl-canvas, .gm-style canvas, .wpgmza_map canvas'
4816    );
4817    var iframeWithMapContent = false;
4818    try {
4819      document
4820        .querySelectorAll('iframe[src*="google.com/maps"]:not([data-ucpf-gated="1"]), iframe[src*="maps.google"]:not([data-ucpf-gated="1"])')
4821        .forEach(function (iframe) {
4822          if (mapIframeIsLive(iframe) && iframe.offsetHeight > 0) {
4823            iframeWithMapContent = true;
4824          }
4825        });
4826    } catch (eIfLive) { /* ignore */ }
4827    if (mapsterLive || canvasLive || iframeWithMapContent) {
4828      mapsHydrated = true;
4829    }
4830  }
4831
4832  /** Mapster canvas / MapLibre map finished past the initial loader. */
4833  function mapsterMapIsLive() {
4834    try {
4835      if (
4836        document.querySelector(
4837          '.mapster-wp-maps .maplibregl-map, .mapster-wp-maps .mapboxgl-map, .mapster-wp-maps-container .maplibregl-map, .mapster-wp-maps-container .mapboxgl-map, .mapster-map .maplibregl-map, .mapster-map .mapboxgl-map'
4838        )
4839      ) {
4840        return true;
4841      }
4842      var hosts = document.querySelectorAll(
4843        '.mapster-wp-maps, [id^="mapster-wp-maps"], .mapster-map'
4844      );
4845      for (var i = 0; i < hosts.length; i++) {
4846        var host = hosts[i];
4847        if (!host || !host.children || !host.children.length) {
4848          continue;
4849        }
4850        var loader = host.querySelector('.mapster-map-loader-initial, .mapster-wp-maps-loader-container .mapster-map-loader-initial');
4851        // Sibling loader outside the map node — check container.
4852        var container = host.closest ? host.closest('.mapster-wp-maps-container') : null;
4853        var containerLoader = container
4854          ? container.querySelector('.mapster-map-loader-initial')
4855          : null;
4856        var stillLoading = !!(
4857          (loader && loader.offsetParent !== null) ||
4858          (containerLoader && containerLoader.offsetParent !== null)
4859        );
4860        if (!stillLoading && host.children.length > 0) {
4861          // Canvas / SVG / div map chrome appeared.
4862          if (host.querySelector('canvas, .maplibregl-canvas, .mapboxgl-canvas, svg')) {
4863            return true;
4864          }
4865        }
4866      }
4867    } catch (eLive) { /* ignore */ }
4868    return false;
4869  }
4870
4871  function whenMapsApiReady(cb) {
4872    var n = 0;
4873    var t = window.setInterval(function () {
4874      n += 1;
4875      var ready =
4876        (window.google && window.google.maps) ||
4877        typeof window.mapboxgl !== 'undefined' ||
4878        typeof window.maplibregl !== 'undefined' ||
4879        (window.L && typeof window.L.map === 'function') ||
4880        document.querySelector(
4881          'iframe[src*="google.com/maps"]:not([data-ucpf-gated="1"]), iframe[src*="maps.google"]:not([data-ucpf-gated="1"]), iframe[data-ucpf-gated="1"][data-src*="maps.google"], iframe[data-ucpf-gated="1"][data-src*="google.com/maps"]'
4882        );
4883      if (ready || n > 40) {
4884        window.clearInterval(t);
4885        cb();
4886      }
4887    }, 100);
4888  }
4889
4890  function ensureMapsIfNeeded() {
4891    if (!hasCategoryConsent('marketing') || !hasCategoryConsent('functional')) {
4892      mapsHydrateStarted = false;
4893      mapsHydrated = false;
4894      mapsHydrateRetryScheduled = false;
4895      return;
4896    }
4897    if (mapsHydrated) {
4898      return;
4899    }
4900    if (mapsHydrateStarted) {
4901      return;
4902    }
4903    var hasMapSurface = !!document.querySelector(
4904      MAP_MARKERS.join(',') +
4905        ',script[data-src*="maps.googleapis"],script[src*="maps.googleapis"],script[data-src*="mapbox"],script[data-src*="mapster"],script[src*="mapster"],script[data-src*="maplibre"],script[src*="maplibre"],iframe[data-src*="google.com/maps"],iframe[data-src*="maps.google"],iframe[data-ucpf-gated="1"][data-src*="maps.google"]'
4906    );
4907    if (!hasMapSurface) {
4908      return;
4909    }
4910    mapsHydrateStarted = true;
4911    // One restore pass + a single delayed retry (idempotent via data-ucpf-map-restored).
4912    whenMapsApiReady(reinitMapWidgets);
4913    window.setTimeout(reinitMapWidgets, 600);
4914    // Mapster/MapLibre: one extra retry if still on the initial loader after APIs activate.
4915    if (!mapsHydrateRetryScheduled) {
4916      mapsHydrateRetryScheduled = true;
4917      window.setTimeout(function () {
4918        if (mapsHydrated) {
4919          return;
4920        }
4921        if (mapsterMapIsLive()) {
4922          mapsHydrated = true;
4923          return;
4924        }
4925        // Allow one more hydrate pass for canvas maps that missed the first refire.
4926        mapsHydrateStarted = false;
4927        whenMapsApiReady(reinitMapWidgets);
4928        window.setTimeout(reinitMapWidgets, 400);
4929      }, 1800);
4930    }
4931  }
4932
4933  /**
4934   * Elementor sticky / Motion FX mutate the DOM constantly (sticky spacers,
4935   * data-settings). Re-scanning guards mid-flight races prepareOptions and
4936   * surfaces as: Cannot read properties of undefined (reading 'translateY'),
4937   * leaving widgets stuck with .elementor-invisible (untouchable on mobile).
4938   */
4939  /**
4940   * Captcha / map / calendly / gated iframe injections must wake refresh even
4941   * when builders mutate the DOM constantly.
4942   * @param {MutationRecord|null} mutation
4943   * @param {Node|null} target
4944   */
4945  function isConsentSurfaceSignal(mutation, target) {
4946    function nodeIsSurface(node) {
4947      if (!node || node.nodeType !== 1) {
4948        return false;
4949      }
4950      var el = /** @type {Element} */ (node);
4951      var tag = String(el.tagName || '').toUpperCase();
4952      if (tag === 'IFRAME') {
4953        var src = (el.getAttribute('src') || el.getAttribute('data-src') || '').toLowerCase();
4954        if (
4955          isVideoPlayerUrl(src) ||
4956          el.getAttribute('data-ucpf-gated') === '1' ||
4957          el.getAttribute('data-ucpf-category') ||
4958          /recaptcha|hcaptcha|turnstile|challenges\.cloudflare|calendly|google\.com\/maps|maps\.google|mapbox|openstreetmap|getjobber|jobber|typeform|jotform|hsforms|tally\.so/.test(
4959            src
4960          )
4961        ) {
4962          return true;
4963        }
4964      }
4965      if (tag === 'SCRIPT') {
4966        if (el.getAttribute('data-ucpf-gated') === '1' || el.getAttribute('form_url') || el.getAttribute('typehub_id')) {
4967          return true;
4968        }
4969      }
4970      if (!el.classList) {
4971        return false;
4972      }
4973      return !!(
4974        el.classList.contains('g-recaptcha') ||
4975        el.classList.contains('cf-turnstile') ||
4976        el.classList.contains('h-captcha') ||
4977        el.classList.contains('grecaptcha-badge') ||
4978        el.classList.contains('calendly-inline-widget') ||
4979        el.classList.contains('jobber-inline-work-request') ||
4980        el.classList.contains('ucpf-iframe-placeholder') ||
4981        el.classList.contains('gm-style') ||
4982        el.classList.contains('mapboxgl-map') ||
4983        el.classList.contains('maplibregl-map') ||
4984        el.classList.contains('mapster-wp-maps') ||
4985        el.classList.contains('mapster-wp-maps-container') ||
4986        el.classList.contains('mapster-map')
4987      );
4988    }
4989    if (target && nodeIsSurface(target)) {
4990      return true;
4991    }
4992    if (!mutation || mutation.type !== 'childList') {
4993      return false;
4994    }
4995    var lists = [mutation.addedNodes, mutation.removedNodes];
4996    for (var li = 0; li < lists.length; li++) {
4997      var list = lists[li];
4998      if (!list || !list.length) {
4999        continue;
5000      }
5001      for (var ni = 0; ni < list.length; ni++) {
5002        if (nodeIsSurface(list[ni])) {
5003          return true;
5004        }
5005        try {
5006          var child = list[ni];
5007          if (child && child.querySelector && child.querySelector('iframe, .g-recaptcha, .cf-turnstile, .h-captcha, .calendly-inline-widget, .jobber-inline-work-request, script[data-ucpf-gated], script[form_url]')) {
5008            return true;
5009          }
5010        } catch (eQ) { /* ignore */ }
5011      }
5012    }
5013    return false;
5014  }
5015
5016  /**
5017   * Elementor sticky / Motion FX mutate the DOM constantly (sticky spacers,
5018   * data-settings). Re-scanning guards mid-flight races prepareOptions and
5019   * surfaces as: Cannot read properties of undefined (reading 'translateY'),
5020   * leaving widgets stuck with .elementor-invisible (untouchable on mobile).
5021   */
5022  function isElementorBuilderNoise(mutation, target) {
5023    // Video / captcha / map signals must still wake the guard.
5024    if (isVideoIframeSignal(mutation, target)) {
5025      return false;
5026    }
5027    if (isConsentSurfaceSignal(mutation, target)) {
5028      return false;
5029    }
5030    if (!mutation) {
5031      return false;
5032    }
5033    if (mutation.type === 'attributes') {
5034      var attr = mutation.attributeName || '';
5035      if (attr === 'data-settings') {
5036        return true;
5037      }
5038      if ((attr === 'src' || attr === 'data-src') && target && target.tagName) {
5039        var tag = String(target.tagName).toUpperCase();
5040        if (tag === 'IFRAME' && isVideoPlayerUrl(target.getAttribute('src') || target.getAttribute('data-src') || '')) {
5041          return false;
5042        }
5043        if (tag === 'AUDIO' || tag === 'VIDEO' || tag === 'SOURCE') {
5044          return true;
5045        }
5046      }
5047      // Ignore transform/style thrash from Motion FX / sticky when leaveBuildersAlone.
5048      if (leaveBuildersAlone() && (attr === 'style' || attr === 'data-motion' || attr.indexOf('data-') === 0)) {
5049        return isBuilderChrome(target);
5050      }
5051      return false;
5052    }
5053    if (mutation.type !== 'childList') {
5054      return false;
5055    }
5056    function isStickySpacer(node) {
5057      return !!(
5058        node &&
5059        node.nodeType === 1 &&
5060        node.classList &&
5061        node.classList.contains('elementor-sticky__spacer')
5062      );
5063    }
5064    function isEmptyText(node) {
5065      return !!(node && node.nodeType === 3 && !String(node.textContent || '').trim());
5066    }
5067    var lists2 = [mutation.addedNodes, mutation.removedNodes];
5068    var saw = false;
5069    for (var lj = 0; lj < lists2.length; lj++) {
5070      var list2 = lists2[lj];
5071      if (!list2 || !list2.length) {
5072        continue;
5073      }
5074      for (var nj = 0; nj < list2.length; nj++) {
5075        var n = list2[nj];
5076        if (isEmptyText(n)) {
5077          continue;
5078        }
5079        saw = true;
5080        if (!isStickySpacer(n)) {
5081          return false;
5082        }
5083      }
5084    }
5085    return saw;
5086  }
5087
5088  /**
5089   * True only for real video player nodes — never bare .elementor-wrapper
5090   * (Elementor uses that class site-wide; matching it re-ran refresh on every
5091   * Motion FX / sticky mutation and tore down captcha covers).
5092   * @param {Element|Node} node
5093   */
5094  function nodeLooksLikeVideoEmbed(node) {
5095    if (!node || node.nodeType !== 1) {
5096      return false;
5097    }
5098    var el = /** @type {Element} */ (node);
5099    if (String(el.tagName || '').toUpperCase() === 'IFRAME') {
5100      return isVideoPlayerUrl(el.getAttribute('src') || el.getAttribute('data-src') || '');
5101    }
5102    if (el.classList) {
5103      if (
5104        el.classList.contains('elementor-widget-video') ||
5105        el.classList.contains('elementor-video') ||
5106        el.classList.contains('elementor-background-video-container') ||
5107        el.classList.contains('elementor-background-video-embed')
5108      ) {
5109        return true;
5110      }
5111      // Inline video shell only — not every .elementor-wrapper on the page.
5112      if (el.classList.contains('elementor-wrapper') && el.classList.contains('elementor-open-inline')) {
5113        return true;
5114      }
5115    }
5116    if (el.getAttribute) {
5117      var wt = el.getAttribute('data-widget_type') || '';
5118      if (wt === 'video.default' || wt === 'video') {
5119        return true;
5120      }
5121    }
5122    return false;
5123  }
5124
5125  /**
5126   * Wake the guard only when a YouTube/Vimeo iframe (or video widget) actually
5127   * appears or changes src — not when Elementor mutates a parent wrapper.
5128   * @param {MutationRecord|null} mutation
5129   * @param {Node|null} target
5130   */
5131  function isVideoIframeSignal(mutation, target) {
5132    if (!mutation) {
5133      return !!(target && String(target.tagName || '').toUpperCase() === 'IFRAME' && nodeLooksLikeVideoEmbed(target));
5134    }
5135    if (mutation.type === 'attributes') {
5136      var attr = mutation.attributeName || '';
5137      if ((attr === 'src' || attr === 'data-src') && target && String(target.tagName || '').toUpperCase() === 'IFRAME') {
5138        return isVideoPlayerUrl(
5139          /** @type {Element} */ (target).getAttribute('src') ||
5140            /** @type {Element} */ (target).getAttribute('data-src') ||
5141            ''
5142        );
5143      }
5144      return false;
5145    }
5146    if (mutation.type !== 'childList') {
5147      return false;
5148    }
5149    var lists = [mutation.addedNodes, mutation.removedNodes];
5150    for (var li = 0; li < lists.length; li++) {
5151      var list = lists[li];
5152      if (!list || !list.length) {
5153        continue;
5154      }
5155      for (var ni = 0; ni < list.length; ni++) {
5156        if (nodeLooksLikeVideoEmbed(list[ni])) {
5157          return true;
5158        }
5159      }
5160    }
5161    return false;
5162  }
5163
5164  /**
5165   * When OS "Reduce motion" is on, Elementor Pro Motion FX may crash and never
5166   * clear .elementor-invisible (visibility:hidden → no taps). Reveal stuck nodes.
5167   * Also run a delayed pass without reduced-motion: gate/consent races can leave
5168   * the same stuck class after Elementor should have animated.
5169   */
5170  function unhideReducedMotionElementor() {
5171    try {
5172      var preferReduce =
5173        window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
5174      var nodes = document.querySelectorAll('.elementor-invisible');
5175      var n = nodes.length;
5176      if (!n) {
5177        return 0;
5178      }
5179      // Immediate unhide only for reduced-motion (Motion FX crash path).
5180      if (!preferReduce) {
5181        return 0;
5182      }
5183      Array.prototype.forEach.call(nodes, function (el) {
5184        el.classList.remove('elementor-invisible');
5185      });
5186      return n;
5187    } catch (eUnhide) {
5188      return 0;
5189    }
5190  }
5191
5192  /** Late safety net: reveal Elementor nodes still invisible long after frontend init. */
5193  function unhideStuckElementorInvisible() {
5194    try {
5195      var nodes = document.querySelectorAll('.elementor-invisible');
5196      if (!nodes.length) {
5197        return 0;
5198      }
5199      // If Elementor never booted (script error / gate race), unhide so the page is usable
5200      // even when the visitor declined marketing cookies (no Accept reload / CF bypass).
5201      var elReady = !!(window.elementorFrontend && elementorFrontend.elementsHandler);
5202      if (elReady && document.readyState !== 'complete') {
5203        return 0;
5204      }
5205      Array.prototype.forEach.call(nodes, function (el) {
5206        el.classList.remove('elementor-invisible');
5207      });
5208      return nodes.length;
5209    } catch (eStuck) {
5210      return 0;
5211    }
5212  }
5213
5214  var consentChangeTimer = null;
5215  var consentChangeRunning = false;
5216
5217  function onConsentChanged() {
5218    // Accept/Reject hard-reload: skip heavy hydrate — it freezes the tab (Mapster/Elementor).
5219    if (window.__ucpfConsentReloadPending) {
5220      try {
5221        refresh();
5222      } catch (eRefreshOnly) { /* ignore */ }
5223      return;
5224    }
5225    // document + window + UCPF.on all fire the same event — coalesce to one pass.
5226    if (consentChangeTimer) {
5227      window.clearTimeout(consentChangeTimer);
5228    }
5229    consentChangeTimer = window.setTimeout(runConsentChanged, 50);
5230  }
5231
5232  function runConsentChanged() {
5233    consentChangeTimer = null;
5234    if (consentChangeRunning || window.__ucpfConsentReloadPending) {
5235      return;
5236    }
5237    consentChangeRunning = true;
5238    try {
5239      // refresh() applies/removes covers and restores parked YouTube/Vimeo iframe src.
5240      refresh();
5241      if (
5242        hasCategoryConsent('security') ||
5243        hasCategoryConsent('functional') ||
5244        hasCategoryConsent('marketing')
5245      ) {
5246        activateLoaderFallback();
5247      }
5248      ensureCalendlyIfNeeded();
5249      ensureCaptchasIfNeeded();
5250      // Always hydrate videos after consent — builders must not block GDPR unlock.
5251      ensureVideosIfNeeded();
5252      ensureSmashBalloonIfNeeded();
5253      // Maps: same class of failure as Vimeo (API parked, widget already ran).
5254      ensureMapsIfNeeded();
5255      ensureAnimationsIfNeeded();
5256      // Layout-only Elementor Motion FX recovery stays behind leaveBuildersAlone.
5257      if (!leaveBuildersAlone()) {
5258        unhideReducedMotionElementor();
5259        window.setTimeout(unhideStuckElementorInvisible, 400);
5260      }
5261      // Ensure GTM4WP Vimeo/YouTube helpers re-run after player APIs activate.
5262      // Skip when loader already ran from the cancelled-navigation fallback.
5263      if (window.UCPFLoader && typeof window.UCPFLoader.applyConsent === 'function') {
5264        try {
5265          window.UCPFLoader.applyConsent();
5266        } catch (eApply) { /* ignore */ }
5267      }
5268      // Force-clear any leftover checkout panels (Safari cancel-navigation path).
5269      if (hasAllCategories(['security', 'functional'])) {
5270        queryAll(CHECKOUT_SELECTORS).forEach(function (node) {
5271          var host = node;
5272          if (node.tagName !== 'FORM') {
5273            host = (node.querySelector && (node.querySelector('form.checkout') || node.querySelector('form.woocommerce-checkout'))) || node;
5274          }
5275          removeGuard(host);
5276          if (host && host !== node) {
5277            removeGuard(node);
5278          }
5279        });
5280        queryAll(['.ucpf-consent-guard--active[data-ucpf-guard-kind="checkout"]']).forEach(function (wrap) {
5281          wrap.classList.remove('ucpf-consent-guard--active');
5282          var panel = wrap.querySelector('.ucpf-consent-guard__panel');
5283          if (panel) {
5284            panel.remove();
5285          }
5286          if (wrap.tagName === 'FORM') {
5287            unlockFields(wrap);
5288          }
5289        });
5290      }
5291    } finally {
5292      consentChangeRunning = false;
5293    }
5294  }
5295
5296  function boot() {
5297    refresh();
5298    resyncAllGuards();
5299    ensureVideosIfNeeded();
5300    ensureSmashBalloonIfNeeded();
5301    ensureMapsIfNeeded();
5302    ensureAnimationsIfNeeded();
5303    ensureCaptchasIfNeeded();
5304    if (!leaveBuildersAlone()) {
5305      unhideReducedMotionElementor();
5306    }
5307    // Banner root + late builder/lazy iframes — re-scan and re-copy tokens.
5308    [50, 250, 800, 2000, 4000].forEach(function (ms) {
5309      window.setTimeout(function () {
5310        refresh();
5311        resyncAllGuards();
5312        ensureVideosIfNeeded();
5313        ensureMapsIfNeeded();
5314        ensureAnimationsIfNeeded();
5315        ensureCaptchasIfNeeded();
5316        if (!leaveBuildersAlone()) {
5317          unhideReducedMotionElementor();
5318          if (ms >= 2000) {
5319            unhideStuckElementorInvisible();
5320          }
5321        }
5322      }, ms);
5323    });
5324    window.addEventListener('load', function () {
5325      ensureVideosIfNeeded();
5326      ensureMapsIfNeeded();
5327      ensureAnimationsIfNeeded();
5328      ensureCaptchasIfNeeded();
5329      window.setTimeout(ensureVideosIfNeeded, 1200);
5330      window.setTimeout(ensureMapsIfNeeded, 1200);
5331      window.setTimeout(ensureAnimationsIfNeeded, 1200);
5332      window.setTimeout(ensureCaptchasIfNeeded, 1200);
5333      if (leaveBuildersAlone()) {
5334        return;
5335      }
5336      unhideReducedMotionElementor();
5337      window.setTimeout(unhideReducedMotionElementor, 1200);
5338      window.setTimeout(unhideStuckElementorInvisible, 2500);
5339      window.setTimeout(unhideStuckElementorInvisible, 5000);
5340    });
5341    // Elementor may finish Motion FX after our early passes.
5342    try {
5343      document.addEventListener('elementor/frontend/init', function () {
5344        window.setTimeout(function () {
5345          refresh();
5346          ensureVideosIfNeeded();
5347          ensureMapsIfNeeded();
5348          ensureAnimationsIfNeeded();
5349          ensureCaptchasIfNeeded();
5350        }, 150);
5351        window.setTimeout(ensureVideosIfNeeded, 900);
5352        window.setTimeout(ensureMapsIfNeeded, 900);
5353        window.setTimeout(ensureAnimationsIfNeeded, 900);
5354        window.setTimeout(ensureCaptchasIfNeeded, 900);
5355        if (!leaveBuildersAlone()) {
5356          window.setTimeout(unhideReducedMotionElementor, 100);
5357          window.setTimeout(unhideReducedMotionElementor, 800);
5358          window.setTimeout(unhideStuckElementorInvisible, 2000);
5359        }
5360      });
5361    } catch (eEl) { /* ignore */ }
5362    bindElementorPopupRemeasure();
5363    document.addEventListener('ucpf:consent:changed', onConsentChanged);
5364    document.addEventListener('ucpf:consent:accepted_all', onConsentChanged);
5365    document.addEventListener('ucpf:consent:rejected_all', onConsentChanged);
5366    window.addEventListener('ucpf:consent:changed', onConsentChanged);
5367    window.addEventListener('ucpf:consent:accepted_all', onConsentChanged);
5368    window.addEventListener('ucpf:consent:rejected_all', onConsentChanged);
5369    var ucpfBound = false;
5370    function bindUcpf() {
5371      if (ucpfBound) {
5372        return true;
5373      }
5374      if (window.UCPF && typeof window.UCPF.on === 'function') {
5375        window.UCPF.on('ucpf:consent:changed', onConsentChanged);
5376        window.UCPF.on('ucpf:consent:accepted_all', onConsentChanged);
5377        window.UCPF.on('ucpf:consent:rejected_all', onConsentChanged);
5378        ucpfBound = true;
5379        return true;
5380      }
5381      return false;
5382    }
5383    if (!bindUcpf()) {
5384      [50, 200, 800, 2000].forEach(function (ms) {
5385        window.setTimeout(bindUcpf, ms);
5386      });
5387    }
5388    // Safari bfcache: re-evaluate guards when restoring a frozen checkout page.
5389    window.addEventListener('pageshow', function () {
5390      captchaReinitScheduled = false;
5391      refresh();
5392      resyncAllGuards();
5393      ensureVideosIfNeeded();
5394      ensureMapsIfNeeded();
5395      ensureCaptchasIfNeeded();
5396      if (!leaveBuildersAlone()) {
5397        unhideReducedMotionElementor();
5398      }
5399    });
5400
5401    if (typeof MutationObserver === 'function') {
5402      var timer = null;
5403      var obs = new MutationObserver(function (mutations) {
5404        var relevant = false;
5405        for (var i = 0; i < mutations.length; i++) {
5406          var m = mutations[i];
5407          var t = m.target;
5408          if (!t || t.nodeType !== 1) {
5409            // childList may only have addedNodes
5410            if (m.type === 'childList' && (m.addedNodes.length || m.removedNodes.length)) {
5411              if (isElementorBuilderNoise(m, t)) {
5412                continue;
5413              }
5414              relevant = true;
5415              break;
5416            }
5417            continue;
5418          }
5419          // Never react to our own UI or Elementor animation class thrash.
5420          if (t.id === 'ucpf-root' || (t.closest && t.closest('#ucpf-root, .ucpf-consent-guard__panel'))) {
5421            continue;
5422          }
5423          if (m.type === 'attributes' && m.attributeName === 'class') {
5424            // Ignore pure Elementor motion / sticky / animation class toggles.
5425            continue;
5426          }
5427          if (isElementorBuilderNoise(m, t)) {
5428            continue;
5429          }
5430          relevant = true;
5431          break;
5432        }
5433        if (!relevant) {
5434          return;
5435        }
5436        if (timer) {
5437          window.clearTimeout(timer);
5438        }
5439        // Longer debounce so Elementor entrance animations can settle.
5440        timer = window.setTimeout(function () {
5441          refresh();
5442          resyncAllGuards();
5443          ensureCalendlyIfNeeded();
5444          remeasureVisiblePopupEmbeds();
5445          unhideReducedMotionElementor();
5446        }, 400);
5447      });
5448      // Do NOT watch `class` — Elementor fade/sticky/motion toggle classes constantly;
5449      // that used to re-scan the whole page and break Motion Effects + Turnstile.
5450      // data-settings is Elementor Motion FX / sticky state — filtered as noise above.
5451      obs.observe(document.documentElement, {
5452        childList: true,
5453        subtree: true,
5454        attributes: true,
5455        attributeFilter: ['data-src', 'src', 'data-settings', 'data-url'],
5456      });
5457    }
5458  }
5459
5460  if (document.readyState === 'loading') {
5461    document.addEventListener('DOMContentLoaded', boot);
5462  } else {
5463    boot();
5464  }
5465})();

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.