PageSourceSearch

https://skylinesmiles.com/_templates/_system/_js/privacy-banner.js

js skylinesmiles.com collected 2026-10-08 21:22:31 UTC 67,263 bytes, 2,016 lines download raw bytes

1// *** ADDED — Consent Mode v2 bootstrap ***
2// Must run BEFORE the privacy IIFE installs its stubs.
3// Uses the real gtag() function call (not raw array push) so that gtag.js
4// registers consent state via its consent API — this is what makes hits
5// report pscdl=granted/denied instead of pscdl=noapi.
6var DI_CONSENT_MODE_V2 = (typeof window.DiConsentMode === 'boolean') ? window.DiConsentMode : true;
7
8window.dataLayer = window.dataLayer || [];
9window.gtag = function () { window.dataLayer.push(arguments); };
10
11window.google_tracking = window.google_tracking || {};
12window.google_tracking.gtag = window.gtag;
13
14// Default: deny everything. Must be called before any Google tag loads.
15window.gtag('consent', 'default', {
16	analytics_storage: 'denied',
17	ad_storage: 'denied',
18	ad_user_data: 'denied',
19	ad_personalization: 'denied',
20	wait_for_update: 500
21});
22
23// Returning visitor with stored consent: update immediately so GTM/GA
24// initialize already knowing consent is granted.
25(function () {
26	try {
27		var name = 'di_privacy_consent';
28		var match = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'));
29		if (match && decodeURIComponent(match[1]) === 'granted') {
30			window.gtag('consent', 'update', {
31				analytics_storage: 'granted',
32				ad_storage: 'granted',
33				ad_user_data: 'granted',
34				ad_personalization: 'granted'
35			});
36		}
37	} catch (e) { }
38})();
39// *** END ADDED — Consent Mode v2 bootstrap ***
40
41(function (window, document) {
42	'use strict';
43
44	/*
45		Privacy / Consent Tracking Controller
46
47		Purpose:
48		- Pending consent: hold known/curated tracking scripts and API calls.
49		- Accepted consent: release held tracking scripts/calls immediately.
50		- Rejected consent: discard held tracking scripts/calls.
51		- GPC / Do Not Track: always treat as denied.
52		- Unknown third-party scripts are not held client-side; they are controlled by the server-side CSP policy.
53
54		Important:
55		- This script intentionally distinguishes between:
56		  1) Non-essential trackers / analytics / pixels / session replay tools.
57		  2) Functional third-party services that are required for the site experience.
58		  3) Unknown third-party resources, which should be blocked by server-side CSP while consent is pending/denied.
59	*/
60
61	var CONSENT_KEY = 'di_privacy_consent';
62	var COOKIE_MAX_AGE = 60 * 60 * 24 * 180;
63
64	var heldScripts = [];
65	var heldCalls = [];
66	var heldSockets = [];
67
68	function _logBlocked() {
69		console.log.apply(console, arguments);
70	}
71
72	function detectGPC() {
73		try {
74			// Global Privacy Control, where supported.
75			if (navigator && typeof navigator.globalPrivacyControl !== 'undefined') {
76				if (navigator.globalPrivacyControl) return true;
77			}
78
79			if (window.matchMedia && window.matchMedia('(global-privacy-control: 1)').matches) {
80				return true;
81			}
82
83			// Legacy browser "Do Not Track" support.
84			var dnt = navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack;
85			if (dnt === '1' || dnt === 'yes') {
86				return true;
87			}
88		} catch (e) { }
89
90		return false;
91	}
92
93	function getStoredConsent() {
94		try {
95			var name = CONSENT_KEY.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
96			var match = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'));
97
98			if (!match) {
99				return null;
100			}
101
102			var v = decodeURIComponent(match[1]);
103
104			if (v === 'granted' || v === 'denied') {
105				return v;
106			}
107		} catch (e) { }
108
109		return null;
110	}
111
112	function setStoredConsent(v) {
113		if (v !== 'granted' && v !== 'denied') {
114			return;
115		}
116
117		/*
118			The consent cookie is the single source of truth.
119
120			It controls:
121			- Server-side CSP mode
122			- JavaScript consent state
123			- Whether the banner appears
124
125			When this cookie expires, consent returns to "pending" and the banner
126			will be shown again.
127		*/
128		try {
129			document.cookie =
130				CONSENT_KEY + '=' + encodeURIComponent(v) +
131				'; Path=/' +
132				'; Max-Age=' + COOKIE_MAX_AGE +
133				'; SameSite=Lax' +
134				(window.location.protocol === 'https:' ? '; Secure' : '');
135		} catch (e) { }
136	}
137
138	function clearStoredConsent() {
139		try {
140			document.cookie =
141				CONSENT_KEY + '=; Path=/; Max-Age=0; SameSite=Lax' +
142				(window.location.protocol === 'https:' ? '; Secure' : '');
143		} catch (e) { }
144	}
145
146	function clearLegacyStoredConsent() {
147		try {
148			localStorage.removeItem(CONSENT_KEY);
149		} catch (e) { }
150	}
151
152	clearLegacyStoredConsent();
153
154	var gpcEnabled = detectGPC();
155	var storedConsent = getStoredConsent();
156	var consent = storedConsent || null;
157
158	function getConsentState() {
159		if (gpcEnabled) return 'denied';
160		return consent || 'pending';
161	}
162
163	function trackingAllowed() {
164		return !gpcEnabled && consent === 'granted';
165	}
166
167	function shouldHold() {
168		return !gpcEnabled && getConsentState() === 'pending';
169	}
170
171	function shouldDrop() {
172		return gpcEnabled || getConsentState() === 'denied';
173	}
174
175	/*
176		Early static script guard.
177
178		This catches hard-coded parser-discovered <script src="..."> tags that appear
179		later in the HTML, before the browser has a chance to execute them.
180
181		This is especially important for trackers added as normal static HTML tags,
182		such as Cloudflare Web Analytics / Browser Insights.
183
184		Note:
185		- This is not meant to replace CSP.
186		- CSP is still the hard server-side fallback.
187		- This guard exists so known static tracker scripts can be held/released
188		  on the same page after consent.
189	*/
190	var earlyStaticScriptPatterns = [
191		/static\.cloudflareinsights\.com\/beacon\.min\.js/i,
192		/js\.zi-scripts\.com\/zi-tag\.js/i
193	];
194
195	function earlyMatchesStaticScript(src) {
196		if (!src) return false;
197
198		var s = String(src);
199
200		for (var i = 0; i < earlyStaticScriptPatterns.length; i++) {
201			if (earlyStaticScriptPatterns[i].test(s)) {
202				return true;
203			}
204		}
205
206		return false;
207	}
208
209	function earlyCloneScriptForRelease(original) {
210		var clone = document.createElement('script');
211
212		for (var i = 0; i < original.attributes.length; i++) {
213			var attr = original.attributes[i];
214
215			try {
216				clone.setAttribute(attr.name, attr.value);
217			} catch (e) { }
218		}
219
220		return clone;
221	}
222
223	function earlyHoldStaticScript(script) {
224		if (!script || !script.src) return false;
225
226		var src = script.src;
227
228		if (!earlyMatchesStaticScript(src)) {
229			return false;
230		}
231
232		for (var i = 0; i < heldScripts.length; i++) {
233			if (heldScripts[i].src === src) {
234				return true;
235			}
236		}
237
238		if (shouldHold()) {
239			heldScripts.push({
240				src: src,
241				node: earlyCloneScriptForRelease(script),
242				parent: document.head || document.documentElement,
243				method: 'appendChild',
244				refNode: null
245			});
246
247			try {
248				if (script.parentNode) {
249					script.parentNode.removeChild(script);
250				}
251			} catch (e) { }
252
253			_logBlocked('[Privacy] Early-held static tracker script until consent →', src);
254			return true;
255		}
256
257		if (shouldDrop()) {
258			try {
259				if (script.parentNode) {
260					script.parentNode.removeChild(script);
261				}
262			} catch (e) { }
263
264			_logBlocked('[Privacy] Early-removed static tracker script →', src);
265			return true;
266		}
267
268		return false;
269	}
270
271	function installEarlyStaticScriptGuard() {
272		function scanExistingScriptsEarly() {
273			var scripts = document.querySelectorAll('script[src]');
274
275			for (var i = 0; i < scripts.length; i++) {
276				earlyHoldStaticScript(scripts[i]);
277			}
278		}
279
280		scanExistingScriptsEarly();
281
282		if (window.MutationObserver && document.documentElement) {
283			try {
284				var earlyObserver = new MutationObserver(function (mutations) {
285					for (var i = 0; i < mutations.length; i++) {
286						var added = mutations[i].addedNodes;
287
288						for (var j = 0; j < added.length; j++) {
289							var node = added[j];
290
291							if (node && node.tagName === 'SCRIPT') {
292								earlyHoldStaticScript(node);
293							}
294
295							if (node && node.querySelectorAll) {
296								var scripts = node.querySelectorAll('script[src]');
297
298								for (var k = 0; k < scripts.length; k++) {
299									earlyHoldStaticScript(scripts[k]);
300								}
301							}
302						}
303					}
304				});
305
306				earlyObserver.observe(document.documentElement, {
307					childList: true,
308					subtree: true
309				});
310			} catch (e) { }
311		}
312	}
313
314	installEarlyStaticScriptGuard();
315
316	/*
317		Beacon/network endpoints.
318
319		These are non-essential analytics, ads, attribution, session replay,
320		heatmap, call tracking, fingerprinting, and marketing automation endpoints.
321
322		While consent is pending or denied, requests matching these patterns are
323		blocked client-side. This prevents known trackers from sending data even if
324		a script tries to fire a fetch/XHR/sendBeacon/image beacon.
325	*/
326	var beaconPatterns = [
327
328		// Google Ads / AdSense / remarketing / conversion endpoints
329		/:\/\/ssl\.google-analytics\.com\//i,
330		/:\/\/stats\.g\.doubleclick\.net\//i,
331		/:\/\/www\.google\.com\/pagead\/1p-user-list/i,
332		/:\/\/www\.google\.com\/pagead\/conversion/i,
333		/:\/\/googleads\.g\.doubleclick\.net\//i,
334		/:\/\/pagead2\.googlesyndication\.com\//i,
335		/:\/\/www\.googlesyndication\.com\/pagead\//i,
336		/:\/\/www\.googleadservices\.com\/pagead\//i,
337		/:\/\/www\.google\.com\/pagead\//i,
338
339		// Meta / Facebook
340		/:\/\/www\.facebook\.com\/tr\?/i,
341		/:\/\/graph\.facebook\.com\/v\d+\.\d+\/events/i,
342
343		// TikTok
344		/:\/\/business\.tiktok\.com\/bt\//i,
345		/:\/\/s\.tiktok\.com\//i,
346
347		// Pinterest
348		/:\/\/log\.pinterest\.com\//i,
349		/:\/\/analytics\.pinterest\.com\//i,
350
351		// LinkedIn
352		/:\/\/px\.ads\.linkedin\.com/i,
353
354		// Snapchat
355		/:\/\/tr\.snapchat\.com\//i,
356
357		// Twitter / X
358		/:\/\/analytics\.twitter\.com\//i,
359		/:\/\/t\.co\/i\/collect/i,
360
361		// Reddit Ads
362		/:\/\/events\.redditmedia\.com\//i,
363		/:\/\/www\.redditstatic\.com\/ads\/pixel\.js/i,
364		/:\/\/alb\.reddit\.com\//i,
365
366		// Heatmaps / session replay / behavior tracking
367		/:\/\/static\.hotjar\.com\/c\/hotjar-/i,
368		/:\/\/track\.hotjar\.com\//i,
369		/:\/\/script\.crazyegg\.com\/pages\//i,
370		/:\/\/rs\.fullstory\.com/i,
371		/:\/\/api\.mouseflow\.com\//i,
372		/:\/\/www\.clarity\.ms\/collect/i,
373		/:\/\/tools\.luckyorange\.com\//i,
374
375		// Marketing automation / CRM / attribution
376		/:\/\/track\.hubspot\.com\//i,
377		/:\/\/api\.hsforms\.com\/submissions/i,
378		/:\/\/piwik\.pardot\.com\//i,
379		/:\/\/go\.pardot\.com\//i,
380		/:\/\/cdn\.segment\.com\/v1\/p\//i,
381		/:\/\/api\.mixpanel\.com\//i,
382		/:\/\/api\.amplitude\.com/i,
383		/:\/\/plausible\.io\/api\/event/i,
384		/:\/\/matomo\.cloud\/matomo\.php/i,
385		/:\/\/.*\/matomo\.php/i,
386		/:\/\/analytics\.liine\.com\//i,
387		/:\/\/instantleadcapture\.com\//i,
388		/:\/\/px\.premion\.com\//i,
389		/:\/\/js\.adsrvr\.org\//i,
390		/:\/\/cdn\.raek\.net\//i,
391		/:\/\/s1\.revenuewell\.com\//i,
392		/:\/\/diffuser-cdn\.app-us1\.com\//i,
393		/:\/\/chimpstatic\.com\//i,
394
395		// Fingerprinting-style endpoints or libraries.
396		/fingerprint/i,
397		/\.fpjs\.io/i,
398
399		// AdRoll
400		/:\/\/s\.adroll\.com\//i,
401		/:\/\/d\.adroll\.com\//i,
402		/:\/\/pixel\.adroll\.com\//i,
403
404		// ZoomInfo
405		/:\/\/ws\.zoominfo\.com\/pixel\//i,
406		/:\/\/ws\.zoominfo\.com\//i,
407
408		// VigLink / affiliate tracking
409		/:\/\/vigl\.us\//i,
410
411		// Trovo tag
412		/:\/\/tag\.trovo-tag\.com\//i,
413
414		// Programmatic adtech / cookie-sync / audience matching
415		/:\/\/x\.bidswitch\.net\/sync/i,
416		/:\/\/pixel\.tapad\.com\/idsync/i,
417		/:\/\/ml314\.com\/utsync\.ashx/i,
418		/:\/\/ps\.eyeota\.net\/match/i,
419		/:\/\/cm\.g\.doubleclick\.net\/pixel/i,
420		/:\/\/dsum-sec\.casalemedia\.com\/rum/i,
421		/:\/\/image2\.pubmatic\.com\/AdServer\/Pug/i,
422		/:\/\/pixel\.rubiconproject\.com\/tap\.php/i,
423		/:\/\/us-u\.openx\.net\/w\/1\.0\/sd/i,
424		/:\/\/sync\.outbrain\.com\/cookie-sync/i,
425		/:\/\/sync\.taboola\.com\//i,
426		/:\/\/eb2\.3lift\.com\/xuid/i,
427		/:\/\/ib\.adnxs\.com\/setuid/i,
428
429		// Patient Prism / call intelligence
430		/:\/\/next-api\.patientprism\.com\/v1\/snippet\//i,
431
432		// First-party legacy referral / page interaction tracking
433		// /:\/\/referrals\.di-api\.com\//i,
434
435		// Identity / ad targeting / profiling
436		/:\/\/pro\.ip-api\.com\/json\//i,
437		/:\/\/alocdn\.com\/.*\/xtarget\/p\.json/i,
438		/:\/\/tag\.link-url\.io\//i,
439		/:\/\/idx\.liadm\.com\//i,
440
441		// Cloudflare Web Analytics / Browser Insights
442		/:\/\/static\.cloudflareinsights\.com\/beacon/i,
443		/:\/\/cloudflareinsights\.com\//i,
444
445		// Additional tracker / analytics / adtech beacon endpoints
446		/:\/\/dni\.logmycalls\.com\//i,
447		/:\/\/insight\.adsrvr\.org\//i,
448		/:\/\/bat\.bing\.com\//i,
449		/:\/\/settings\.luckyorange\.com\//i,
450		/:\/\/app\.operadds\.com\//i,
451		/:\/\/js\.zi-scripts\.com\//i,
452		/:\/\/analytics-[0-9]+\.kxcdn\.com\//i,
453		/:\/\/alice\.liine\.com\//i,
454		/:\/\/data\.hockeystack\.com\/send/i,
455		/:\/\/jelly\.mdhv\.io\//i,
456		/:\/\/tag\.simpli\.fi\//i,
457		/:\/\/i\.simpli\.fi\/p/i,
458		/:\/\/um\.simpli\.fi\//i,
459		/:\/\/www\.redditstatic\.com\/ads\/pixel\.js/i,
460		/:\/\/dni\.logmycalls\.com\//i,
461		/:\/\/a\.usbrowserspeed\.com\/cs/i,
462		/:\/\/.*\.usbrowserspeed\.com\/cs/i,
463
464		// Pendo product analytics
465		/:\/\/cdn\.pendo\.io\//i,
466		/:\/\/data\.pendo\.io\//i,
467		/:\/\/app\.pendo\.io\//i,
468
469		// Piwik PRO analytics / tag manager
470		/:\/\/.*\.piwik\.pro\//i,
471
472		// Leadpages analytics
473		/:\/\/api\.leadpages\.io\/analytics\//i,
474
475		// BrandCDN / conversion pixel
476		/:\/\/adservices\.brandcdn\.com\/pixel\//i,
477
478		// LiveRamp / identity sync
479		/:\/\/idsync\.rlcdn\.com\//i,
480
481		// Kissmetrics analytics
482		/:\/\/i\.kissmetrics\.io\//i,
483		/:\/\/scripts\.kissmetrics\.io\//i,
484		/:\/\/trk\.kissmetrics\.io\//i,
485		/:\/\/et\.kissmetrics\.io\//i,
486
487		// RLETS / call tracking / attribution storage iframe
488		/:\/\/[^\/]+\.rlets\.com\/static\/storage\.html/i,
489
490		// ReachLocal / LocaliQ capture, attribution, geolocation enrichment
491		/:\/\/capture-api\.reachlocalservices\.com\//i,
492		
493		// EXTENDED LIST ---
494		// Scheduling / booking widgets
495		/:\/\/[^\/]*calendly\.com\//i,
496		/:\/\/assets\.calendly\.com\//i,
497		/:\/\/[^\/]*localmed\.com\//i,
498		/:\/\/frontend\.assets\.nexhealth\.com\//i,
499		/:\/\/schedule\.jarvisanalytics\.com\//i,
500		/:\/\/embed\.acuityscheduling\.com\//i,
501		/:\/\/cdn\.timetrade\.com\//i,
502		/:\/\/book\.getweave\.com\//i,
503		/:\/\/book2\.getweave\.com\//i,
504		/:\/\/book\.gke1-west3\.wsf-prod-1\.wstack\.net\//i,
505		/:\/\/nextgen\.clixondental\.com\//i,
506		/:\/\/olb\.saloniris\.com\//i,
507		/:\/\/plugin\.mysalononline\.com\//i,
508
509		// Zocdoc
510		/:\/\/api\.zocdoc\.com\//i,
511		/:\/\/api2\.zocdoc\.com\//i,
512		/:\/\/bob\.zocdoc\.com\//i,
513		/:\/\/static\.zocdoc\.com\//i,
514		/:\/\/www\.zocdoc\.com\//i,
515		/:\/\/book\.zocdoc\.com\//i,
516		/:\/\/offsiteschedule\.zocdoc\.com\//i,
517		/:\/\/[^\/]+\.zocdoc\.com\//i,
518
519		// Chat / chatbot / AI assistant widgets
520		/:\/\/connect\.podium\.com\//i,
521		/:\/\/platform\.swellcx\.com\//i,
522		/:\/\/cdn\.apigateway\.co\//i,
523		/:\/\/chat\.gocloudgain\.com\//i,
524		/:\/\/chat\.dentina\.ai\//i,
525		/:\/\/static\.ai\.getdeardoc\.com\//i,
526		/:\/\/chat\.getdeardoc\.com\//i,
527		/:\/\/api\.chat\.getdeardoc\.com\//i,
528		/:\/\/app\.dasconsultantsusa\.com\//i,
529		/:\/\/server\.dasconsultantsusa\.com\//i,
530		/:\/\/app\.purechat\.com\//i,
531		/:\/\/widgetapi\.purechat\.com\//i,
532		/:\/\/[^\/]*botsrv\.com\//i,
533		/:\/\/botsrv2\.com\//i,
534		/:\/\/static\.botsrv2\.com\//i,
535		/:\/\/app\.botsify\.com\//i,
536		/:\/\/widget\.botsify\.com\//i,
537		/:\/\/embed\.tawk\.to\//i,
538		/:\/\/static-v\.tawk\.to\//i,
539		/:\/\/va\.tawk\.to\//i,
540		/:\/\/[^\/]+\.tawk\.to\//i,
541		/:\/\/[^\/]*formilla\.com\//i,
542		/:\/\/widgets\.leadconnectorhq\.com\//i,
543		/:\/\/services\.leadconnectorhq\.com\//i,
544		/:\/\/services\.msgsndr\.com\//i,
545		/:\/\/app\.chatsupport\.co\//i,
546		/:\/\/[^\/]*truelark\.com\//i,
547		/:\/\/ops\.truelark\.com\//i,
548		/:\/\/[^\/]*247aireceptionist\.com\//i,
549		/:\/\/code\.jivosite\.com\//i,
550		/:\/\/[^\/]+\.jivosite\.com\//i,
551		/:\/\/api\.livechatinc\.com\//i,
552		/:\/\/cdn\.livechatinc\.com\//i,
553		/:\/\/[^\/]+\.livechatinc\.com\//i,
554		/:\/\/chat\.doctorsinternet\.com\/web-bot\//i,
555
556		// Forms / lead capture / embedded form systems
557		/:\/\/webform\.adit\.com\//i,
558		/:\/\/embed\.lpcontent\.net\//i,
559		/:\/\/api\.leadpages\.io\//i,
560		/:\/\/app-widgets\.jotform\.io\//i,
561		/:\/\/widgets\.jotform\.io\//i,
562		/:\/\/forms\.liine\.com\//i,
563		/:\/\/dim\.dentalimplantmachine\.co\//i,
564		/:\/\/[^\/]+\.kit\.com\//i,
565		/:\/\/[^\/]+\.ck\.page\//i,
566		/:\/\/[^\/]*convertkit\.com\//i,
567		/:\/\/app\.convertkit\.com\//i,
568		/:\/\/f\.convertkit\.com\//i,
569		/:\/\/api\.hsforms\.com\//i,
570		/:\/\/forms\.hsforms\.com\//i,
571		/:\/\/js\.hsforms\.net\//i,
572
573		// Reviews / reputation / social proof widgets
574		/:\/\/go\.doctorsinternet\.com\//i,
575		/:\/\/go\.pimm-usa\.com\//i,
576		/:\/\/widgets-v7\.birdeye\.com\//i,
577		/:\/\/webchat\.birdeye\.com\//i,
578		/:\/\/widget-v7-cdn\.birdeye\.com\//i,
579		/:\/\/[^\/]*birdeye\.com\//i,
580		/:\/\/widgets\.revue\.us\//i,
581		/:\/\/widgets\.thereviewsplace\.com\//i,
582		/:\/\/assets\.thereviewsplace\.com\//i,
583		/:\/\/api\.thereviewsplace\.com\//i,
584		/:\/\/providers\.doctor\.com\//i,
585		/:\/\/app\.operadds\.com\//i,
586		/:\/\/reviewsonmywebsite\.com\//i,
587		/:\/\/localimpact\.com\//i,
588		/:\/\/local\.demandforce\.com\//i,
589		/:\/\/www\.demandforced3\.com\//i,
590		/:\/\/[^\/]*elfsight\.com\//i,
591		/:\/\/[^\/]*elfsightcdn\.com\//i,
592
593		// Social widgets / embeds
594		/:\/\/www\.instagram\.com\//i,
595		/:\/\/www\.tiktok\.com\//i,
596		/:\/\/lf16-tiktok-web\.tiktokcdn-us\.com\//i,
597		/:\/\/www\.facebook\.com\/plugins\//i,
598		/:\/\/widgets\.pinterest\.com\//i,
599		/:\/\/www\.yelp\.com\//i,
600
601		// Financing / payment / membership widgets
602		/:\/\/files\.withcherry\.com\//i,
603		/:\/\/gql\.withcherry\.com\//i,
604		/:\/\/master-cherry-storage\.s3\.us-west-2\.amazonaws\.com\//i,
605		/:\/\/member\.kleer\.com\//i,
606		/:\/\/member\.clerri\.com\//i,
607		/:\/\/checkout\.staxpayments\.com\//i,
608		/:\/\/goblueswipe\.transactiongateway\.com\//i,
609		/:\/\/secure\.networkmerchants\.com\//i,
610		/:\/\/apply\.sunbit\.com\//i,
611		/:\/\/www\.paypal\.com\//i,
612		/:\/\/www\.paypalobjects\.com\//i,
613		/:\/\/www\.carecredit\.com\//i,
614
615		// Patient / health / practice widgets that may create sessions or lead context
616		/:\/\/patient\.phreesia\.com\//i,
617		/:\/\/patient\.klara\.com\//i,
618		/:\/\/widget\.klara\.com\//i,
619		/:\/\/patient-api\.speareducation\.com\//i,
620		/:\/\/api\.modento\.io\//i,
621		/:\/\/app\.neuralityhealth\.ai\//i,
622		/:\/\/www\.smilevirtual\.com\//i,
623		/:\/\/[^\/]*doctible\.com\//i,
624		/:\/\/[^\/]*yapiapp\.io\//i,
625		
626		// Intake / scheduling / patient forms
627		/:\/\/[^\/]*intakeq\.com\//i,
628		/:\/\/app\.prod\.intelibly\.com\//i,
629		/:\/\/app\.intelibly\.com\//i,
630		/:\/\/sc-widget\.prod\.intelibly\.com\//i,
631		/:\/\/clients-cdn\.pplpractice\.com\//i,
632		/:\/\/webrefillplugin\.bestrxconnect\.com\//i,
633		/:\/\/view\.implantsmiles\.co\//i,
634
635		// Simplifeye / patient communication
636		/:\/\/embed\.simplifeye\.co\//i,
637		/:\/\/api\.simplifeye\.co\//i,
638
639		// AI chat / chatbot / web assistant widgets
640		/:\/\/aichatbotweb\.revenuewell\.com\//i,
641		/:\/\/[^\/]+\.web-assistant\.tensorlinks\.app\//i,
642		/:\/\/[^\/]*tensorlinks\.app\//i,
643		/:\/\/api\.heygent\.ai\//i,
644		/:\/\/lexicon-prod\.apigateway\.co\//i,
645		/:\/\/conversation-prod\.apigateway\.co\//i,
646		/:\/\/widget\.osam\.one\//i,
647
648		// Local marketing / lead widgets
649		/:\/\/www\.localmarketingmanager\.com\//i,
650		/:\/\/[^\/]*localmarketingmanager\.com\//i,
651
652		// Health / patient commerce / referral widgets
653		/:\/\/assets\.fullscript\.com\//i,
654		/:\/\/www\.opencare\.com\//i,
655		/:\/\/[^\/]*opencare\.com\//i
656		// END EXTENDED LIST ---
657	];
658
659	if (!DI_CONSENT_MODE_V2) {
660		beaconPatterns.push(
661			/:\/\/www\.google-analytics\.com\/collect/i,
662			/:\/\/www\.google-analytics\.com\/g\/collect/i,
663			/:\/\/www\.googletagmanager\.com\/collect/i,
664			/:\/\/region1\.google-analytics\.com\/g\/collect/i,
665			/:\/\/region2\.google-analytics\.com\/g\/collect/i,
666			/:\/\/analytics\.google\.com\/g\/collect/i
667		);
668	}
669	/*
670		Script loader patterns.
671
672		These are known non-essential tracker scripts. While consent is pending,
673		the script is held instead of loaded. If consent is accepted, held scripts
674		are released on the same page. If consent is denied, held scripts are discarded.
675	*/
676	var blockUrls = [
677
678		// Google Ads / AdSense / remarketing scripts
679		/google\.com\/pagead\/tag\/js/i,
680		/googleads\.g\.doubleclick\.net\//i,
681		/pagead2\.googlesyndication\.com\//i,
682		/pagead2\.googlesyndication\.com\/pagead\//i,
683		/www\.googlesyndication\.com\/pagead\//i,
684		/www\.googleadservices\.com\/pagead\//i,
685
686		// Meta / Facebook
687		/connect\.facebook\.net\/.*\/fbevents\.js/i,
688		/facebook\.com\/tr\/?/i,
689
690		// TikTok
691		/business\.tiktok\.com\/widget/i,
692		/s\.tiktok\.com\//i,
693		/analytics\.tiktok\.com\/i18n\/pixel\/events\.js/i,
694
695		// LinkedIn
696		/snap\.licdn\.com\/li\.js/i,
697		/snap\.licdn\.com\/li\.lms-analytics\/insight\.min\.js/i,
698
699		// Twitter / X
700		/static\.ads-twitter\.com\/uwt\.js/i,
701
702		// Pinterest
703		/assets\.pinterest\.com\/js\/pinit\.js/i,
704
705		// Snapchat
706		/sc-static\.net\/connect\.js/i,
707
708		// Heatmaps / session replay / user behavior analytics
709		/static\.hotjar\.com\/c\/hotjar-/i,
710		/tools\.luckyorange\.com\/core\/lo\.js/i,
711		/fullstory\.com\/s\/fs\.js/i,
712		/clarity\.ms\/tag/i,
713		/script\.crazyegg\.com\/pages\//i,
714		/mouseflow\.com\/projects\//i,
715
716		// Analytics / CDP / product analytics
717		/cdn\.mixpanel\.com\/lib/i,
718		/cdn\.amplitude\.com\/js/i,
719		/cdn\.segment\.com\/analytics/i,
720		/plausible\.io\/js\/script/i,
721
722		// Call tracking / DNI / lead capture / attribution
723		/tctm\.co\/t\.js/i,
724		/appcontent\.practicenumbers\.com\/phone_number_replace_/i,
725		/cdn\.callrail\.com\/companies\/.+\/swap\.js/i,
726		/cdn\.calltrk\.com\/companies\/.+\/swap\.js/i,
727		/cdn\.rlets\.com\/capture_configs\//i,
728		/analytics\.liine\.com\/v1\/bootstrapped/i,
729		/instantleadcapture\.com\/px\.min\.js/i,
730		/js\.adsrvr\.org\/up_loader/i,
731		/px\.premion\.com\/pixel\.js/i,
732
733		// Anti-fraud / visitor ID / attribution-style scripts
734		/www\.clickcease\.com\/monitor\/.+\.js/i,
735		/cdn\.raek\.net\/js\/raek\.min\.js/i,
736		/hockeystack(@latest)?\/hockeystack\.min\.js/i,
737
738		// Chat/CX scripts that include explicit tracking behavior.
739		/cdn\.livechatinc\.com\/tracking\.js/i,
740		/static\.mobilemonkey\.com\/js\/business_/i,
741		/static\.zdassets\.com\/ekr\/snippet\.js/i,
742		/platform\.swellcx\.com\/widget\/js\/chat\.js/i,
743		/cdn\.apigateway\.co\/webchat-client/i,
744
745		// Marketing automation / email / CRM
746		/js\.hubspot\.com\/analytics/i,
747		/js\.hs-scripts\.com\/\d+\.js/i,
748		/js\.hsforms\.net\/forms\/embed\/v2\.js/i,
749		/chimpstatic\.com\/mcjs-connected\/js\//i,
750		/diffuser-cdn\.app-us1\.com\/diffuser\/diffuser\.js/i,
751		/tag\.brandcdn\.com\/autoscript\//i,
752		/tag\.validate\.audio\/validate\.js/i,
753		/s1\.revenuewell\.com\/scripts\/js-api\.js/i,
754
755		// BrandCDN conversion pixel script
756		/adservices\.brandcdn\.com\/pixel\/cv_pixel\.js/i,
757
758		// AdRoll
759		/s\.adroll\.com\/j\/[^\/]+\/roundtrip\.js/i,
760
761		// ZoomInfo pixel
762		/ws\.zoominfo\.com\/pixel\//i,
763
764		// Trovo tag iframe/script
765		/tag\.trovo-tag\.com\//i,
766
767		// Patient Prism / call intelligence
768		/next-api\.patientprism\.com\/v1\/snippet\//i,
769
770		// Cloudflare Web Analytics / Browser Insights
771		/static\.cloudflareinsights\.com\/beacon\.min\.js/i,
772
773		// Additional tracker scripts
774		/dni\.logmycalls\.com\/dni\.js/i,
775		/insight\.adsrvr\.org\//i,
776		/bat\.bing\.com\/bat\.js/i,
777		/settings\.luckyorange\.com\//i,
778		/app\.operadds\.com\//i,
779		/js\.zi-scripts\.com\/zi-tag\.js/i,
780		/analytics-[0-9]+\.kxcdn\.com\/analytics\.js/i,
781		/tag\.simpli\.fi\//i,
782		/i\.simpli\.fi\/p/i,
783		/www\.redditstatic\.com\/ads\/pixel\.js/i,
784		/dni\.logmycalls\.com\/dni\.js/i,
785
786		// Pendo product analytics
787		/cdn\.pendo\.io\/agent\/static\/[^\/]+\/pendo\.js/i,
788
789		// Piwik PRO container / analytics
790		/containers\.piwik\.pro\/[^\/]+\.js/i,
791
792		// Kissmetrics analytics scripts
793		/i\.kissmetrics\.io\/i\.js/i,
794		/scripts\.kissmetrics\.io\/.*\.js/i,
795		
796		// EXTENDED LIST ---
797		// Scheduling / booking widgets
798		/calendly\.com/i,
799		/assets\.calendly\.com/i,
800		/localmed\.com/i,
801		/frontend\.assets\.nexhealth\.com/i,
802		/schedule\.jarvisanalytics\.com/i,
803		/embed\.acuityscheduling\.com/i,
804		/cdn\.timetrade\.com/i,
805		/book\.getweave\.com/i,
806		/book2\.getweave\.com/i,
807		/book\.gke1-west3\.wsf-prod-1\.wstack\.net/i,
808		/nextgen\.clixondental\.com/i,
809		/olb\.saloniris\.com/i,
810		/plugin\.mysalononline\.com/i,
811
812		// Zocdoc
813		/zocdoc\.com/i,
814		/zocdocstatic\.com/i,
815		/static\.zocdoc\.com/i,
816		/offsiteschedule\.zocdoc\.com/i,
817		/book\.zocdoc\.com/i,
818
819		// Chat / chatbot / AI assistant widgets
820		/connect\.podium\.com/i,
821		/platform\.swellcx\.com/i,
822		/cdn\.apigateway\.co/i,
823		/chat\.gocloudgain\.com/i,
824		/chat\.dentina\.ai/i,
825		/static\.ai\.getdeardoc\.com/i,
826		/chat\.getdeardoc\.com/i,
827		/api\.chat\.getdeardoc\.com/i,
828		/app\.dasconsultantsusa\.com/i,
829		/server\.dasconsultantsusa\.com/i,
830		/app\.purechat\.com/i,
831		/widgetapi\.purechat\.com/i,
832		/botsrv\.com/i,
833		/botsrv2\.com/i,
834		/static\.botsrv2\.com/i,
835		/app\.botsify\.com/i,
836		/widget\.botsify\.com/i,
837		/embed\.tawk\.to/i,
838		/static-v\.tawk\.to/i,
839		/va\.tawk\.to/i,
840		/formilla\.com/i,
841		/widgets\.leadconnectorhq\.com/i,
842		/services\.leadconnectorhq\.com/i,
843		/services\.msgsndr\.com/i,
844		/app\.chatsupport\.co/i,
845		/truelark\.com/i,
846		/ops\.truelark\.com/i,
847		/247aireceptionist\.com/i,
848		/code\.jivosite\.com/i,
849		/jivosite\.com/i,
850		/api\.livechatinc\.com/i,
851		/cdn\.livechatinc\.com/i,
852		/chat\.doctorsinternet\.com/i,
853
854		// Forms / lead capture / embedded form systems
855		/webform\.adit\.com/i,
856		/embed\.lpcontent\.net/i,
857		/api\.leadpages\.io/i,
858		/app-widgets\.jotform\.io/i,
859		/widgets\.jotform\.io/i,
860		/forms\.liine\.com/i,
861		/dim\.dentalimplantmachine\.co/i,
862		/kit\.com/i,
863		/ck\.page/i,
864		/convertkit\.com/i,
865		/app\.convertkit\.com/i,
866		/f\.convertkit\.com/i,
867		/api\.hsforms\.com/i,
868		/forms\.hsforms\.com/i,
869		/js\.hsforms\.net/i,
870
871		// Reviews / reputation / social proof widgets
872		/go\.doctorsinternet\.com/i,
873		/go\.pimm-usa\.com/i,
874		/widgets-v7\.birdeye\.com/i,
875		/webchat\.birdeye\.com/i,
876		/widget-v7-cdn\.birdeye\.com/i,
877		/birdeye\.com/i,
878		/widgets\.revue\.us/i,
879		/widgets\.thereviewsplace\.com/i,
880		/assets\.thereviewsplace\.com/i,
881		/api\.thereviewsplace\.com/i,
882		/providers\.doctor\.com/i,
883		/app\.operadds\.com/i,
884		/reviewsonmywebsite\.com/i,
885		/localimpact\.com/i,
886		/local\.demandforce\.com/i,
887		/demandforced3\.com/i,
888		/elfsight\.com/i,
889		/elfsightcdn\.com/i,
890
891		// Social widgets / embeds
892		/instagram\.com/i,
893		/tiktok\.com/i,
894		/lf16-tiktok-web\.tiktokcdn-us\.com/i,
895		/facebook\.com\/plugins/i,
896		/widgets\.pinterest\.com/i,
897		/www\.yelp\.com/i,
898
899		// Financing / payment / membership widgets
900		/files\.withcherry\.com/i,
901		/gql\.withcherry\.com/i,
902		/master-cherry-storage\.s3\.us-west-2\.amazonaws\.com/i,
903		/member\.kleer\.com/i,
904		/member\.clerri\.com/i,
905		/checkout\.staxpayments\.com/i,
906		/goblueswipe\.transactiongateway\.com/i,
907		/secure\.networkmerchants\.com/i,
908		/apply\.sunbit\.com/i,
909		/www\.paypal\.com/i,
910		/www\.paypalobjects\.com/i,
911		/www\.carecredit\.com/i,
912
913		// Patient / health / practice widgets that may create sessions or lead context
914		/patient\.phreesia\.com/i,
915		/patient\.klara\.com/i,
916		/widget\.klara\.com/i,
917		/patient-api\.speareducation\.com/i,
918		/api\.modento\.io/i,
919		/app\.neuralityhealth\.ai/i,
920		/www\.smilevirtual\.com/i,
921		/doctible\.com/i,
922		/yapiapp\.io/i,
923		
924		// Intake / scheduling / patient forms
925		/intakeq\.com/i,
926		/app\.prod\.intelibly\.com/i,
927		/app\.intelibly\.com/i,
928		/sc-widget\.prod\.intelibly\.com/i,
929		/clients-cdn\.pplpractice\.com/i,
930		/webrefillplugin\.bestrxconnect\.com/i,
931		/view\.implantsmiles\.co/i,
932
933		// Simplifeye / patient communication
934		/embed\.simplifeye\.co/i,
935		/api\.simplifeye\.co/i,
936
937		// AI chat / chatbot / web assistant widgets
938		/aichatbotweb\.revenuewell\.com/i,
939		/web-assistant\.tensorlinks\.app/i,
940		/tensorlinks\.app/i,
941		/api\.heygent\.ai/i,
942		/lexicon-prod\.apigateway\.co/i,
943		/conversation-prod\.apigateway\.co/i,
944		/widget\.osam\.one/i,
945
946		// Local marketing / lead widgets
947		/www\.localmarketingmanager\.com/i,
948		/localmarketingmanager\.com/i,
949
950		// Health / patient commerce / referral widgets
951		/assets\.fullscript\.com/i,
952		/www\.opencare\.com/i,
953		/opencare\.com/i
954		// END EXTENDED LIST ---
955	];
956
957
958	if (!DI_CONSENT_MODE_V2) {
959		blockUrls.push(
960			/googletagmanager\.com\/gtag\/js/i,
961			/googletagmanager\.com\/gtm\.js/i
962		);
963	}
964
965	/*
966		First-party analytics websocket.
967	*/
968	var socketPatterns = [
969		// /^wss:\/\/socket\.di-api\.com\/analytics/i
970	];
971
972	function matchesPattern(url, patterns) {
973		if (!url) return false;
974
975		var s = String(url);
976
977		return patterns.some(function (r) {
978			return r.test(s);
979		});
980	}
981
982	function shouldBlockUrl(url) {
983		return matchesPattern(url, beaconPatterns);
984	}
985
986	function shouldBlockScript(url) {
987		return matchesPattern(url, blockUrls);
988	}
989
990	function shouldHoldSocket(url) {
991		return matchesPattern(url, socketPatterns);
992	}
993
994	function holdScript(src, node, parent, method, refNode) {
995		if (!src) return;
996
997		for (var i = 0; i < heldScripts.length; i++) {
998			if (heldScripts[i].src === src) return;
999		}
1000
1001		heldScripts.push({
1002			src: src,
1003			node: node || null,
1004			parent: parent || null,
1005			method: method || 'appendChild',
1006			refNode: refNode || null
1007		});
1008
1009		_logBlocked('[Privacy] Held tracker script until consent →', src);
1010	}
1011
1012	function holdCall(type, args) {
1013		heldCalls.push({
1014			type: type,
1015			args: Array.prototype.slice.call(args)
1016		});
1017
1018		_logBlocked('[Privacy] Held tracker call until consent →', type);
1019	}
1020
1021	function holdSocket(url, protocols) {
1022		for (var i = 0; i < heldSockets.length; i++) {
1023			if (heldSockets[i].url === url) return;
1024		}
1025
1026		heldSockets.push({
1027			url: url,
1028			protocols: protocols
1029		});
1030
1031		_logBlocked('[Privacy] Held analytics WebSocket until consent →', url);
1032	}
1033
1034	function blockOrHoldScript(src, node, parent, method, refNode) {
1035		if (!shouldBlockScript(src)) return false;
1036
1037		if (shouldHold()) {
1038			holdScript(src, node, parent, method, refNode);
1039			return true;
1040		}
1041
1042		if (shouldDrop()) {
1043			_logBlocked('[Privacy] Blocked tracker script →', src);
1044			return true;
1045		}
1046
1047		return false;
1048	}
1049
1050	function blockOrHoldCall(type, args) {
1051		if (trackingAllowed()) return false;
1052
1053		if (shouldHold()) {
1054			holdCall(type, args);
1055			return true;
1056		}
1057
1058		if (shouldDrop()) {
1059			_logBlocked('[Privacy] Blocked tracker call →', type);
1060			return true;
1061		}
1062
1063		return false;
1064	}
1065
1066	var origCreateElement = document.createElement;
1067	var proto = Element.prototype;
1068	var origAppend = proto.appendChild;
1069	var origInsert = proto.insertBefore;
1070	var origAppendMethod = proto.append;
1071	var origPrependMethod = proto.prepend;
1072	var OrigWebSocket = window.WebSocket || null;
1073
1074	function cloneScriptForRelease(original) {
1075		var clone = origCreateElement.call(document, 'script');
1076
1077		for (var i = 0; i < original.attributes.length; i++) {
1078			var attr = original.attributes[i];
1079
1080			if (attr.name === 'src') {
1081				continue;
1082			}
1083
1084			try {
1085				clone.setAttribute(attr.name, attr.value);
1086			} catch (e) { }
1087		}
1088
1089		clone.src = original.src;
1090
1091		return clone;
1092	}
1093
1094	function holdStaticScriptTag(script) {
1095		if (!script || !script.src) return false;
1096
1097		var src = script.src;
1098
1099		if (!shouldBlockScript(src)) {
1100			return false;
1101		}
1102
1103		for (var i = 0; i < heldScripts.length; i++) {
1104			if (heldScripts[i].src === src) {
1105				return true;
1106			}
1107		}
1108
1109		if (shouldHold()) {
1110			heldScripts.push({
1111				src: src,
1112				node: cloneScriptForRelease(script),
1113				parent: document.head || document.documentElement,
1114				method: 'appendChild',
1115				refNode: null
1116			});
1117
1118			try {
1119				if (script.parentNode) {
1120					script.parentNode.removeChild(script);
1121				}
1122			} catch (e) { }
1123
1124			_logBlocked('[Privacy] Held static tracker script until consent →', src);
1125			return true;
1126		}
1127
1128		if (shouldDrop()) {
1129			try {
1130				if (script.parentNode) {
1131					script.parentNode.removeChild(script);
1132				}
1133			} catch (e) { }
1134
1135			_logBlocked('[Privacy] Removed static tracker script →', src);
1136			return true;
1137		}
1138
1139		return false;
1140	}
1141
1142	function scanStaticTrackerScripts() {
1143		var scripts = document.querySelectorAll('script[src]');
1144
1145		for (var i = 0; i < scripts.length; i++) {
1146			holdStaticScriptTag(scripts[i]);
1147		}
1148	}
1149
1150	function restoreDataLayerPush() {
1151		window.dataLayer = window.dataLayer || [];
1152
1153		try {
1154			window.dataLayer.push = Array.prototype.push;
1155		} catch (e) { }
1156	}
1157
1158	function releaseHeldScripts() {
1159		var scripts = heldScripts.slice();
1160		heldScripts = [];
1161
1162		for (var i = 0; i < scripts.length; i++) {
1163			var item = scripts[i];
1164
1165			try {
1166				var node = item.node;
1167
1168				if (!node) {
1169					node = origCreateElement.call(document, 'script');
1170					node.src = item.src;
1171					node.async = true;
1172				}
1173
1174				if (item.method === 'insertBefore' && item.parent && item.refNode) {
1175					origInsert.call(item.parent, node, item.refNode);
1176				} else if (item.parent) {
1177					origAppend.call(item.parent, node);
1178				} else {
1179					origAppend.call(document.head || document.documentElement, node);
1180				}
1181
1182				_logBlocked('[Privacy] Released tracker script after consent →', item.src);
1183			} catch (e) {
1184				_logBlocked('[Privacy] Failed to release tracker script →', item.src, e);
1185			}
1186		}
1187	}
1188
1189	function releaseHeldSockets() {
1190		var sockets = heldSockets.slice();
1191		heldSockets = [];
1192
1193		if (!OrigWebSocket) return;
1194
1195		for (var i = 0; i < sockets.length; i++) {
1196			try {
1197				if (sockets[i].protocols !== undefined) {
1198					new OrigWebSocket(sockets[i].url, sockets[i].protocols);
1199				} else {
1200					new OrigWebSocket(sockets[i].url);
1201				}
1202
1203				_logBlocked('[Privacy] Released analytics WebSocket after consent →', sockets[i].url);
1204			} catch (e) {
1205				_logBlocked('[Privacy] Failed to release analytics WebSocket →', sockets[i].url, e);
1206			}
1207		}
1208	}
1209
1210	function replayHeldCalls() {
1211		var calls = heldCalls.slice();
1212		heldCalls = [];
1213
1214		restoreDataLayerPush();
1215
1216		for (var i = 0; i < calls.length; i++) {
1217			var item = calls[i];
1218
1219			try {
1220				if (item.type === 'dataLayer.push') {
1221					Array.prototype.push.apply(window.dataLayer, item.args);
1222					continue;
1223				}
1224
1225				if (item.type === 'gtag' || item.type === 'google_tracking.gtag') {
1226					window.dataLayer = window.dataLayer || [];
1227					Array.prototype.push.call(window.dataLayer, item.args);
1228					continue;
1229				}
1230
1231				if ((item.type === 'fbq' || item.type === 'facebook_tracking.fbq') && typeof window.fbq === 'function') {
1232					window.fbq.apply(window, item.args);
1233					continue;
1234				}
1235
1236				if (item.type === 'analytics.track' && window.analytics && typeof window.analytics.track === 'function') {
1237					window.analytics.track.apply(window.analytics, item.args);
1238					continue;
1239				}
1240
1241				if (item.type === 'analytics.identify' && window.analytics && typeof window.analytics.identify === 'function') {
1242					window.analytics.identify.apply(window.analytics, item.args);
1243					continue;
1244				}
1245
1246				if (item.type === 'analytics.page' && window.analytics && typeof window.analytics.page === 'function') {
1247					window.analytics.page.apply(window.analytics, item.args);
1248					continue;
1249				}
1250
1251				if (item.type === 'mixpanel.track' && window.mixpanel && typeof window.mixpanel.track === 'function') {
1252					window.mixpanel.track.apply(window.mixpanel, item.args);
1253					continue;
1254				}
1255
1256				if (item.type === 'amplitude.logEvent' && window.amplitude && typeof window.amplitude.logEvent === 'function') {
1257					window.amplitude.logEvent.apply(window.amplitude, item.args);
1258					continue;
1259				}
1260
1261				if (item.type === 'plausible' && typeof window.plausible === 'function') {
1262					window.plausible.apply(window, item.args);
1263					continue;
1264				}
1265			} catch (e) {
1266				_logBlocked('[Privacy] Failed to replay tracker call →', item.type, e);
1267			}
1268		}
1269	}
1270
1271	function acceptTracking() {
1272		if (gpcEnabled) {
1273			_logBlocked('[Privacy] Consent grant ignored because GPC/DNT is enabled.');
1274			return;
1275		}
1276
1277		consent = 'granted';
1278		setStoredConsent('granted');
1279
1280		// *** ADDED — Consent Mode v2 update signal ***
1281		// Use the real gtag() function (defined before the IIFE) so gtag.js
1282		// registers consent via its API. Must fire before releaseHeldScripts
1283		// so GA loads already knowing consent is granted.
1284		try {
1285			if (typeof window.gtag === 'function') {
1286				console.log('[Privacy] Updating Consent Mode v2 signals for granted consent.');
1287				window.gtag('consent', 'update', {
1288					analytics_storage: 'granted',
1289					ad_storage: 'granted',
1290					ad_user_data: 'granted',
1291					ad_personalization: 'granted'
1292				});
1293			}
1294		} catch (e) { }
1295		// *** END ADDED — Consent Mode v2 update signal ***
1296
1297		restoreDataLayerPush();
1298		releaseHeldScripts();
1299		releaseHeldSockets();
1300
1301		setTimeout(function () {
1302			replayHeldCalls();
1303		}, 800);
1304
1305		_logBlocked('[Privacy] Tracking accepted. Held trackers released.');
1306	}
1307
1308	function denyTracking() {
1309		consent = 'denied';
1310		setStoredConsent('denied');
1311
1312		heldScripts = [];
1313		heldCalls = [];
1314		heldSockets = [];
1315
1316		_logBlocked('[Privacy] Tracking denied. Held trackers discarded.');
1317	}
1318
1319	var privacy = {
1320		gpcEnabled: gpcEnabled,
1321
1322		get consent() {
1323			return getConsentState();
1324		},
1325
1326		get trackingAllowed() {
1327			return trackingAllowed();
1328		},
1329
1330		get heldScripts() {
1331			return heldScripts.slice();
1332		},
1333
1334		get heldCalls() {
1335			return heldCalls.slice();
1336		},
1337
1338		get heldSockets() {
1339			return heldSockets.slice();
1340		},
1341
1342		setConsent: function (value) {
1343			if (value === 'granted') {
1344				acceptTracking();
1345				return;
1346			}
1347
1348			if (value === 'denied') {
1349				denyTracking();
1350			}
1351		},
1352
1353		accept: acceptTracking,
1354		deny: denyTracking,
1355		release: acceptTracking,
1356
1357		reset: function () {
1358			clearStoredConsent();
1359			consent = null;
1360			heldScripts = [];
1361			heldCalls = [];
1362			heldSockets = [];
1363		}
1364	};
1365
1366	window.DiPrivacy = privacy;
1367
1368	// *** ADDED — Early return for granted users ***
1369	// If user has already granted consent, skip installing all the stubs and blockers.
1370	// The Consent Mode v2 default + update was already pushed via the real gtag()
1371	// before the IIFE started, so GTM/GA will read granted state on init.
1372	if (trackingAllowed()) {
1373		return;
1374	}
1375	// *** END ADDED — Early return for granted users ***
1376
1377	console.log('[Privacy] Tracking is held/blocked. Consent state →', getConsentState());
1378
1379	// sendBeacon is commonly used for analytics hits and page-unload beacons.
1380	if (navigator.sendBeacon) {
1381		var origBeacon = navigator.sendBeacon;
1382
1383		navigator.sendBeacon = function (url, data) {
1384			if (!trackingAllowed() && shouldBlockUrl(url)) {
1385				_logBlocked('[Privacy] Blocked sendBeacon →', url);
1386				return true;
1387			}
1388
1389			return origBeacon.apply(this, arguments);
1390		};
1391	}
1392
1393	// fetch() is commonly used by modern analytics, CDPs, pixels, and session replay tools.
1394	if (window.fetch) {
1395		var origFetch = window.fetch;
1396
1397		window.fetch = function (input, init) {
1398			var url = (typeof input === 'string') ? input : (input && input.url);
1399
1400			if (!trackingAllowed() && shouldBlockUrl(url)) {
1401				_logBlocked('[Privacy] Blocked fetch →', url);
1402				return Promise.resolve(new Response(null, { status: 204 }));
1403			}
1404
1405			return origFetch.apply(this, arguments).catch(function () {
1406				return new Response(null, { status: 204 });
1407			});
1408		};
1409	}
1410
1411	// XHR is still used by older analytics and marketing scripts.
1412	if (window.XMLHttpRequest) {
1413		var OrigXHR = XMLHttpRequest;
1414
1415		function WrappedXHR() {
1416			var xhr = new OrigXHR();
1417			var _open = xhr.open;
1418			var _send = xhr.send;
1419
1420			xhr.open = function (method, url) {
1421				xhr._blockedUrl = url;
1422				xhr._blockThis = !trackingAllowed() && shouldBlockUrl(url);
1423				return _open.apply(xhr, arguments);
1424			};
1425
1426			xhr.send = function () {
1427				if (xhr._blockThis) {
1428					_logBlocked('[Privacy] Blocked XHR →', xhr._blockedUrl);
1429
1430					try {
1431						xhr.abort();
1432					} catch (e) { }
1433
1434					return;
1435				}
1436
1437				return _send.apply(xhr, arguments);
1438			};
1439
1440			return xhr;
1441		}
1442
1443		WrappedXHR.prototype = OrigXHR.prototype;
1444		window.XMLHttpRequest = WrappedXHR;
1445	}
1446
1447	// Some trackers still fire using image beacon URLs.
1448	if (window.HTMLImageElement) {
1449		var imgProto = HTMLImageElement.prototype;
1450		var desc = Object.getOwnPropertyDescriptor(imgProto, 'src');
1451
1452		if (desc && desc.set) {
1453			Object.defineProperty(imgProto, 'src', {
1454				get: desc.get,
1455				set: function (url) {
1456					if (!trackingAllowed() && shouldBlockUrl(url)) {
1457						_logBlocked('[Privacy] Blocked Image →', url);
1458						return url;
1459					}
1460
1461					return desc.set.call(this, url);
1462				},
1463				configurable: true,
1464				enumerable: true
1465			});
1466		}
1467	}
1468
1469	// Consent-gated WebSockets. Only matching socketPatterns are held before consent.
1470	if (OrigWebSocket) {
1471		window.WebSocket = function (url, protocols) {
1472			if (!trackingAllowed() && shouldHoldSocket(url)) {
1473				if (shouldHold()) {
1474					holdSocket(url, protocols);
1475				} else {
1476					_logBlocked('[Privacy] Blocked analytics WebSocket →', url);
1477				}
1478
1479				return {
1480					close: function () { },
1481					send: function () { },
1482					addEventListener: function () { },
1483					removeEventListener: function () { },
1484					dispatchEvent: function () { return false; },
1485					readyState: 3,
1486					bufferedAmount: 0,
1487					url: String(url || '')
1488				};
1489			}
1490
1491			if (protocols !== undefined) {
1492				return new OrigWebSocket(url, protocols);
1493			}
1494
1495			return new OrigWebSocket(url);
1496		};
1497
1498		window.WebSocket.prototype = OrigWebSocket.prototype;
1499	}
1500
1501	// jQuery.getScript() is used by many older site snippets to load trackers dynamically.
1502	if (window.jQuery && window.jQuery.getScript) {
1503		var $ = window.jQuery;
1504		var origGetScript = $.getScript;
1505
1506		$.getScript = function (url, cb) {
1507			if (blockOrHoldScript(url, null, null, 'jqueryGetScript', null)) {
1508				return;
1509			}
1510
1511			return origGetScript.apply(this, arguments);
1512		};
1513	}
1514
1515	// Dynamically inserted <script src> trackers are held or blocked.
1516	(function () {
1517		proto.appendChild = function (node) {
1518			if (node && node.tagName === 'SCRIPT') {
1519				var src = node.src || node.getAttribute('src');
1520
1521				if (blockOrHoldScript(src, node, this, 'appendChild', null)) {
1522					return node;
1523				}
1524			}
1525
1526			return origAppend.call(this, node);
1527		};
1528
1529		proto.insertBefore = function (node, ref) {
1530			if (node && node.tagName === 'SCRIPT') {
1531				var src = node.src || node.getAttribute('src');
1532
1533				if (blockOrHoldScript(src, node, this, 'insertBefore', ref)) {
1534					return node;
1535				}
1536			}
1537
1538			return origInsert.call(this, node, ref);
1539		};
1540
1541		if (origAppendMethod) {
1542			proto.append = function () {
1543				for (var i = 0; i < arguments.length; i++) {
1544					var node = arguments[i];
1545
1546					if (node && node.tagName === 'SCRIPT') {
1547						var src = node.src || node.getAttribute('src');
1548
1549						if (blockOrHoldScript(src, node, this, 'appendChild', null)) {
1550							return;
1551						}
1552					}
1553				}
1554
1555				return origAppendMethod.apply(this, arguments);
1556			};
1557		}
1558
1559		if (origPrependMethod) {
1560			proto.prepend = function () {
1561				for (var i = 0; i < arguments.length; i++) {
1562					var node = arguments[i];
1563
1564					if (node && node.tagName === 'SCRIPT') {
1565						var src = node.src || node.getAttribute('src');
1566
1567						if (blockOrHoldScript(src, node, this, 'appendChild', null)) {
1568							return;
1569						}
1570					}
1571				}
1572
1573				return origPrependMethod.apply(this, arguments);
1574			};
1575		}
1576	})();
1577
1578	// Static/parser-inserted tracker scripts are scanned separately.
1579	// This helps catch hard-coded <script src="..."> tags that are not loaded through appendChild/insertBefore.
1580	scanStaticTrackerScripts();
1581
1582	if (document.readyState === 'loading') {
1583		document.addEventListener('DOMContentLoaded', scanStaticTrackerScripts);
1584	}
1585
1586	// Watch for tracker scripts added later by widgets or inline code.
1587	// This catches scripts that are inserted into the page after our initial scan.
1588	if (window.MutationObserver) {
1589		try {
1590			var trackerScriptObserver = new MutationObserver(function (mutations) {
1591				for (var i = 0; i < mutations.length; i++) {
1592					var added = mutations[i].addedNodes;
1593
1594					for (var j = 0; j < added.length; j++) {
1595						var node = added[j];
1596
1597						if (node && node.tagName === 'SCRIPT') {
1598							holdStaticScriptTag(node);
1599						}
1600
1601						if (node && node.querySelectorAll) {
1602							var scripts = node.querySelectorAll('script[src]');
1603
1604							for (var k = 0; k < scripts.length; k++) {
1605								holdStaticScriptTag(scripts[k]);
1606							}
1607						}
1608					}
1609				}
1610			});
1611
1612			trackerScriptObserver.observe(document.documentElement, {
1613				childList: true,
1614				subtree: true
1615			});
1616		} catch (e) { }
1617	}
1618
1619	function trackerStub(name) {
1620		return function () {
1621			blockOrHoldCall(name, arguments);
1622		};
1623	}
1624
1625	/*
1626		Common tracker APIs.
1627
1628		These stubs prevent known trackers from firing before consent.
1629		When consent is pending, calls are held. When consent is denied or GPC/DNT
1630		is active, calls are discarded. When accepted, held calls are replayed.
1631	*/
1632	window.dataLayer = window.dataLayer || [];
1633
1634	window.dataLayer.push = function () {
1635		// *** ADDED — Consent Mode v2 / GTM / gtag bypass ***
1636		// Allow consent signals, GTM internals, and gtag config/js/set/get
1637		// through unconditionally. GTM needs unobstructed dataLayer.push
1638		// to initialize itself and read consent state. Consent signals
1639		// must never be held or dropped.
1640		var arg = arguments[0];
1641
1642		// Always allow consent signals through — Consent Mode v2 must not be gated.
1643		// gtag('consent', 'default'|'update', {...}) arrives as arguments where arg[0] === 'consent'.
1644		var isConsentCommand = arg && (
1645			arg[0] === 'consent' ||
1646			arg['0'] === 'consent'
1647		);
1648
1649		// Always allow GTM internal events through (gtm.start, gtm.js, gtm.dom, gtm.load, etc.)
1650		var isGtmInternal = arg && typeof arg === 'object' && !Array.isArray(arg) && (
1651			'gtm.start' in arg ||
1652			'gtm.uniqueEventId' in arg ||
1653			(arg.event && typeof arg.event === 'string' && arg.event.indexOf('gtm.') === 0)
1654		);
1655
1656		// Allow gtag config/js/set/get commands so gtag.js can initialize properly.
1657		// gtag('js', new Date()) and gtag('config', 'G-XXX') need to reach dataLayer.
1658		var isGtagInternal = arg && (
1659			arg[0] === 'js' ||
1660			arg[0] === 'config' ||
1661			arg[0] === 'set' ||
1662			arg[0] === 'get' ||
1663			arg['0'] === 'js' ||
1664			arg['0'] === 'config' ||
1665			arg['0'] === 'set' ||
1666			arg['0'] === 'get'
1667		);
1668
1669		if (isConsentCommand || isGtmInternal || isGtagInternal) {
1670			return Array.prototype.push.apply(window.dataLayer, arguments);
1671		}
1672		// *** END ADDED — Consent Mode v2 / GTM / gtag bypass ***
1673
1674		if (blockOrHoldCall('dataLayer.push', arguments)) {
1675			return;
1676		}
1677
1678		return Array.prototype.push.apply(window.dataLayer, arguments);
1679	};
1680
1681	// *** ADDED — gtag stubs intentionally not installed ***
1682	// NOTE: window.gtag and window.google_tracking.gtag were defined as real
1683	// functions BEFORE this IIFE ran (in the Consent Mode v2 bootstrap block).
1684	// We do NOT overwrite them with stubs here — they need to remain real so
1685	// that gtag.js can register consent via its API (which is what produces
1686	// pscdl=denied/granted on hits instead of pscdl=noapi).
1687	//
1688	// Tracking gating for gtag still happens via the wrapped dataLayer.push
1689	// above: consent/gtm/gtag-internal commands pass through, everything else
1690	// (e.g. gtag('event', 'conversion', {...})) goes through blockOrHoldCall.
1691	// *** END ADDED — gtag stubs intentionally not installed ***
1692
1693	window.fbq = trackerStub('fbq');
1694	window.facebook_tracking = window.facebook_tracking || {};
1695	window.facebook_tracking.fbq = trackerStub('facebook_tracking.fbq');
1696
1697	window.FB = window.FB || { AppEvents: {} };
1698	window.FB.AppEvents.logEvent = trackerStub('FB.AppEvents.logEvent');
1699	window.FB.AppEvents.logPurchase = trackerStub('FB.AppEvents.logPurchase');
1700
1701	window.twttr = window.twttr || {};
1702	window.twttr.track = trackerStub('twttr.track');
1703	window.twttr.load = trackerStub('twttr.load');
1704
1705	window._linkedin_data_partner_ids = [];
1706	window.linkedinData = trackerStub('linkedinData');
1707
1708	window.pintrk = trackerStub('pintrk');
1709	window.ttq = trackerStub('ttq');
1710	window.snaptr = trackerStub('snaptr');
1711	window.hj = trackerStub('hj');
1712
1713	window.mixpanel = window.mixpanel || {};
1714	window.mixpanel.track = trackerStub('mixpanel.track');
1715	window.mixpanel.identify = trackerStub('mixpanel.identify');
1716	window.mixpanel.register = trackerStub('mixpanel.register');
1717
1718	window.amplitude = window.amplitude || {};
1719	window.amplitude.logEvent = trackerStub('amplitude.logEvent');
1720	window.amplitude.setUserProperties = trackerStub('amplitude.setUserProperties');
1721
1722	window.analytics = window.analytics || {};
1723	window.analytics.track = trackerStub('analytics.track');
1724	window.analytics.identify = trackerStub('analytics.identify');
1725	window.analytics.page = trackerStub('analytics.page');
1726
1727	window.plausible = trackerStub('plausible');
1728
1729})(window, document);
1730
1731(function () {
1732
1733	// Set window.privacy_allow = { disabled: true } before this script to suppress the banner entirely
1734	var privacy_allow = Object.assign({ disabled: false }, window.privacy_allow || {});
1735	window.privacy_allow = privacy_allow;
1736
1737	// Per-site override API — call before or after this script:
1738	//   privacy_config.set('variant', 'manage');
1739	if (!window.privacy_config || typeof window.privacy_config !== 'object') window.privacy_config = {};
1740	Object.defineProperty(window.privacy_config, 'set', {
1741		enumerable: false, configurable: true,
1742		value: function (key, val) { window.privacy_config[key] = val; }
1743	});
1744
1745	// variant: 'simple' → Decline / Accept All banner (no modal)
1746	// variant: 'manage' → Manage / Accept All banner + preferences modal
1747	var privacy_config_defaults = {
1748		primaryColor: '#1e3a5f',
1749		bgColor: '#ffffff',
1750		textColor: '#374151',
1751		fontFamily: 'inherit',
1752		buttonRadius: '4px',
1753		privacyPolicyUrl: '/privacy/',
1754		variant: 'manage',
1755		bannerText: 'We use cookies and tracking technologies to improve your browsing experience on our website, to show you personalized content, and to analyze our website traffic. Read our [privacy policy] to learn more.',
1756		declineLabel: 'Decline',
1757		manageLabel: 'Manage',
1758		acceptLabel: 'Accept All',
1759		modalTitle: 'Your tracking preferences.',
1760		modalDesc: 'We use tracking technologies, including scripts that may not set cookies, to understand how you use our site and deliver relevant content. You can learn more in our',
1761		submitLabel: 'Submit Preferences',
1762		rejectLabel: 'Reject all',
1763		cancelLabel: 'Cancel',
1764		categories: [
1765			{
1766				id: 'tracking',
1767				label: 'Tracking & Analytics',
1768				description: 'Scripts and technologies that help us understand how visitors interact with our site, measure performance, and serve relevant content and ads.',
1769				defaultOn: true,
1770			}
1771		]
1772	};
1773	var privacy_config;
1774
1775	// ─── helpers ───────────────────────────────────────────────────────────────
1776
1777	function privacy_cb_el(tag, props) {
1778		var node = document.createElement(tag);
1779		if (props) Object.keys(props).forEach(function (k) { node[k] = props[k]; });
1780		return node;
1781	}
1782
1783	function privacy_cb_removeBanner() {
1784		var el = document.getElementById('di-consent-banner');
1785		if (el) el.parentNode.removeChild(el);
1786	}
1787
1788	function privacy_cb_removeModal() {
1789		var el = document.getElementById('di-cookie-pref-dialog');
1790		if (!el) return;
1791		try { el.close(); } catch (e) { }
1792	}
1793
1794	function privacy_cb_accept() {
1795		try { if (window.DiPrivacy && typeof window.DiPrivacy.accept === 'function') window.DiPrivacy.accept(); } catch (e) { }
1796		privacy_cb_removeModal(); privacy_cb_removeBanner();
1797	}
1798
1799	function privacy_cb_reject() {
1800		try { if (window.DiPrivacy && typeof window.DiPrivacy.deny === 'function') window.DiPrivacy.deny(); } catch (e) { }
1801		privacy_cb_removeModal(); privacy_cb_removeBanner();
1802	}
1803
1804	function privacy_cb_rejectAll() {
1805		privacy_config.categories.forEach(function (cat) {
1806			var t = document.getElementById('di-cm-' + cat.id + '-toggle');
1807			if (t) t.checked = false;
1808		});
1809		privacy_cb_reject();
1810	}
1811
1812	function privacy_cb_savePreferences() {
1813		var anyOff = privacy_config.categories.some(function (cat) {
1814			var t = document.getElementById('di-cm-' + cat.id + '-toggle');
1815			return t && !t.checked;
1816		});
1817		anyOff ? privacy_cb_reject() : privacy_cb_accept();
1818	}
1819
1820	// ─── styles ────────────────────────────────────────────────────────────────
1821
1822	function privacy_cb_injectStyles() {
1823		var existing = document.getElementById('di-consent-banner-styles');
1824		if (existing) existing.parentNode.removeChild(existing);
1825		var style = document.createElement('style');
1826		style.id = 'di-consent-banner-styles';
1827		var css = '';
1828
1829		css += '#di-consent-banner{position:fixed;bottom:0;left:0;right:0;z-index:9999999999;background:var(--di-cb-bg);box-shadow:0 -4px 16px rgba(0,0,0,.10);padding:20px 24px;font-family:var(--di-cb-font);box-sizing:border-box;}';
1830		css += '#di-consent-banner .di-cb__inner{display:flex;flex-direction:column;align-items:center;justify-content:space-between;gap:32px;max-width:1280px;margin:0 auto;}';
1831		css += '@media(min-width:640px){#di-consent-banner .di-cb__inner{flex-direction:row;}}';
1832		css += '#di-consent-banner .di-cb__text{font-size:14px;color:var(--di-cb-text);margin:0;}';
1833		css += '#di-consent-banner .di-cb__learn{color:var(--di-cb-primary);text-decoration:underline;transition:opacity .2s ease;}';
1834		css += '#di-consent-banner .di-cb__learn:hover{opacity:.8;}';
1835		css += '#di-consent-banner .di-cb__actions{display:flex;gap:8px!important;flex-shrink:0;}';
1836		css += '@media(max-width:639px){#di-consent-banner .di-cb__actions{width:100%;flex-direction:column-reverse;}}';
1837		css += '#di-consent-banner .di-cb__btn{cursor:pointer;font-size:16px;font-weight:500;padding:12px 24px;border-radius:var(--di-cb-radius);border:1px solid transparent;line-height:1;white-space:nowrap;transition:background .2s ease,color .2s ease,opacity .2s ease;}';
1838		css += '#di-consent-banner .di-cb__btn--secondary{background:transparent;border-color:var(--di-cb-primary);color:var(--di-cb-primary);}';
1839		css += '#di-consent-banner .di-cb__btn--secondary:hover{background:var(--di-cb-primary);color:#fff;}';
1840		css += '#di-consent-banner .di-cb__btn--primary{background:var(--di-cb-primary);border-color:var(--di-cb-primary);color:#fff;}';
1841		css += '#di-consent-banner .di-cb__btn--primary:hover{opacity:.88;}';
1842
1843		css += '@keyframes di-cm-fadein{from{opacity:0}
1843to{opacity:1}}';
1844		css += '@keyframes di-cm-slidein{from{transform:translateY(10px);opacity:0}to{transform:translateY(0);opacity:1}}';
1845		css += '#di-cookie-pref-dialog{border:none;padding:0;margin:auto;background:#fff;border-radius:10px;box-shadow:0 8px 40px rgba(0,0,0,.22);max-width:480px;width:calc(100vw - 32px);box-sizing:border-box;overflow:hidden;font-family:var(--di-cb-font);animation:di-cm-slidein .2s ease;}';
1846		css += '#di-cookie-pref-dialog::backdrop{background:rgba(0,0,0,.45);animation:di-cm-fadein .18s ease;}';
1847		css += '#di-cookie-pref-dialog .di-cm__header{padding:22px 24px 0;}';
1848		css += '#di-cookie-pref-dialog .di-cm__title-row{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;}';
1849		css += '#di-cookie-pref-dialog .di-cm__title{font-size:19px;font-weight:700;color:#111827;margin:0 0 8px;line-height:1.3;}';
1850		css += '#di-cookie-pref-dialog .di-cm__close{background:none;border:none;cursor:pointer;color:#9ca3af;font-size:22px;line-height:1;padding:0;flex-shrink:0;margin-top:2px;transition:color .15s ease;}';
1851		css += '#di-cookie-pref-dialog .di-cm__close:hover{color:#374151;}';
1852		css += '#di-cookie-pref-dialog .di-cm__desc{font-size:13px;color:#6b7280;margin:0;line-height:1.55;padding:0 24px;}';
1853		css += '#di-cookie-pref-dialog .di-cm__desc a{color:var(--di-cb-primary);text-decoration:underline;}';
1854		css += '#di-cookie-pref-dialog .di-cm__rows{margin-top:16px;border-top:1px solid #f3f4f6;}';
1855		css += '#di-cookie-pref-dialog .di-cm__item{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:16px 24px;border-bottom:1px solid #f3f4f6;}';
1856		css += '#di-cookie-pref-dialog .di-cm__item-info{flex:1;}';
1857		css += '#di-cookie-pref-dialog .di-cm__item-label{font-size:14px;font-weight:600;color:#111827;margin:0 0 4px;}';
1858		css += '#di-cookie-pref-dialog .di-cm__item-desc{font-size:12px;color:#6b7280;margin:0;line-height:1.5;}';
1859		css += '#di-cookie-pref-dialog .di-cm__toggle-wrap{flex-shrink:0;position:relative;width:44px;height:24px;margin-top:2px;}';
1860		css += '#di-cookie-pref-dialog .di-cm__toggle-wrap input{opacity:0;width:0;height:0;position:absolute;}';
1861		css += '#di-cookie-pref-dialog .di-cm__toggle-track{position:absolute;inset:0;background:#d1d5db;border-radius:999px;cursor:pointer;transition:background .2s ease;}';
1862		css += '#di-cookie-pref-dialog .di-cm__toggle-track::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2);transition:transform .2s ease;}';
1863		css += '#di-cookie-pref-dialog .di-cm__toggle-wrap input:checked+.di-cm__toggle-track{background:var(--di-cb-primary);}';
1864		css += '#di-cookie-pref-dialog .di-cm__toggle-wrap input:checked+.di-cm__toggle-track::after{transform:translateX(20px);}';
1865		css += '#di-cookie-pref-dialog .di-cm__footer{display:flex;gap:8px;padding:16px 24px;flex-wrap:wrap;}';
1866		css += '#di-cookie-pref-dialog .di-cm__btn{cursor:pointer;font-size:14px;font-weight:500;padding:10px 16px;border-radius:var(--di-cb-radius);border:1px solid transparent;line-height:1;white-space:nowrap;flex:1;transition:background .2s ease,color .2s ease,opacity .2s ease;}';
1867		css += '#di-cookie-pref-dialog .di-cm__btn--cancel{background:transparent;border-color:#d1d5db;color:#374151;}';
1868		css += '#di-cookie-pref-dialog .di-cm__btn--cancel:hover{background:#f9fafb;}';
1869		css += '#di-cookie-pref-dialog .di-cm__btn--reject{background:transparent;border-color:var(--di-cb-primary);color:var(--di-cb-primary);}';
1870		css += '#di-cookie-pref-dialog .di-cm__btn--reject:hover{background:var(--di-cb-primary);color:#fff;}';
1871		css += '#di-cookie-pref-dialog .di-cm__btn--submit{background:var(--di-cb-primary);border-color:var(--di-cb-primary);color:#fff;}';
1872		css += '#di-cookie-pref-dialog .di-cm__btn--submit:hover{opacity:.88;}';
1873		css += '#di-cookie-pref-dialog .di-cm__policy{padding:0 24px 16px;font-size:11px;color:#9ca3af;}';
1874		css += '#di-cookie-pref-dialog .di-cm__policy a{color:#9ca3af;text-decoration:underline;}';
1875		css += '#di-cookie-pref-dialog .di-cm__policy a:hover{color:#6b7280;}';
1876
1877		style.textContent = css;
1878		document.head.appendChild(style);
1879	}
1880
1881	// ─── modal ─────────────────────────────────────────────────────────────────
1882
1883	function privacy_cb_createDialog() {
1884		var dialog = privacy_cb_el('dialog', { id: 'di-cookie-pref-dialog' });
1885		dialog.style.setProperty('--di-cb-primary', privacy_config.primaryColor);
1886		dialog.style.setProperty('--di-cb-radius', privacy_config.buttonRadius);
1887		dialog.style.setProperty('--di-cb-font', privacy_config.fontFamily);
1888
1889		dialog.addEventListener('click', function (e) {
1890			var r = dialog.getBoundingClientRect();
1891			if (e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom) privacy_cb_removeModal();
1892		});
1893		dialog.addEventListener('cancel', function (e) { e.preventDefault(); privacy_cb_removeModal(); });
1894
1895		var titleRow = privacy_cb_el('div', { className: 'di-cm__title-row' });
1896		var titleEl = privacy_cb_el('p', { className: 'di-cm__title', textContent: privacy_config.modalTitle });
1897		var closeBtn = privacy_cb_el('button', { className: 'di-cm__close', type: 'button', innerHTML: '&times;' });
1898		closeBtn.setAttribute('aria-label', 'Close');
1899		closeBtn.addEventListener('click', privacy_cb_removeModal);
1900		titleRow.appendChild(titleEl);
1901		titleRow.appendChild(closeBtn);
1902
1903		var header = privacy_cb_el('div', { className: 'di-cm__header' });
1904		header.appendChild(titleRow);
1905
1906		var desc = privacy_cb_el('p', { className: 'di-cm__desc' });
1907		desc.appendChild(document.createTextNode(privacy_config.modalDesc + ' '));
1908		desc.appendChild(privacy_cb_el('a', { href: privacy_config.privacyPolicyUrl, textContent: 'Privacy Policy' }));
1909		desc.appendChild(document.createTextNode('.'));
1910
1911		var rows = privacy_cb_el('div', { className: 'di-cm__rows' });
1912		var toggleInputs = [];
1913		privacy_config.categories.forEach(function (cat) {
1914			var row = privacy_cb_el('div', { className: 'di-cm__item' });
1915			var info = privacy_cb_el('div', { className: 'di-cm__item-info' });
1916			var lbl = privacy_cb_el('p', { className: 'di-cm__item-label', textContent: cat.label });
1917			var dsc = privacy_cb_el('p', { className: 'di-cm__item-desc', textContent: cat.description });
1918			var wrap = privacy_cb_el('label', { className: 'di-cm__toggle-wrap' });
1919			var input = privacy_cb_el('input', { type: 'checkbox', id: 'di-cm-' + cat.id + '-toggle', checked: cat.defaultOn !== false });
1920			var track = privacy_cb_el('span', { className: 'di-cm__toggle-track' });
1921			toggleInputs.push(input);
1922			info.appendChild(lbl); info.appendChild(dsc);
1923			wrap.appendChild(input); wrap.appendChild(track);
1924			row.appendChild(info); row.appendChild(wrap);
1925			rows.appendChild(row);
1926		});
1927
1928		var footer = privacy_cb_el('div', { className: 'di-cm__footer' });
1929		var cancelBtn = privacy_cb_el('button', { className: 'di-cm__btn di-cm__btn--cancel', type: 'button', textContent: privacy_config.cancelLabel });
1930		var rejectBtn = privacy_cb_el('button', { className: 'di-cm__btn di-cm__btn--reject', type: 'button', textContent: privacy_config.rejectLabel });
1931		rejectBtn.setAttribute('data-event', 'consent_reject');
1932		var submitBtn = privacy_cb_el('button', { className: 'di-cm__btn di-cm__btn--submit', type: 'button', textContent: privacy_config.submitLabel });
1933		function privacy_cb_updateSubmitEvent() {
1934			var anyOff = toggleInputs.some(function (t) { return !t.checked; });
1935			submitBtn.setAttribute('data-event', anyOff ? 'consent_reject' : 'consent_accept');
1936		}
1937		privacy_cb_updateSubmitEvent();
1938		toggleInputs.forEach(function (t) { t.addEventListener('change', privacy_cb_updateSubmitEvent); });
1939		cancelBtn.addEventListener('click', privacy_cb_removeModal);
1940		rejectBtn.addEventListener('click', privacy_cb_rejectAll);
1941		submitBtn.addEventListener('click', privacy_cb_savePreferences);
1942		footer.appendChild(cancelBtn); footer.appendChild(rejectBtn); footer.appendChild(submitBtn);
1943
1944		var policyFooter = privacy_cb_el('div', { className: 'di-cm__policy' });
1945		policyFooter.appendChild(privacy_cb_el('a', { href: privacy_config.privacyPolicyUrl, textContent: 'Privacy Policy' }));
1946
1947		dialog.appendChild(header);
1948		dialog.appendChild(desc);
1949		dialog.appendChild(rows);
1950		dialog.appendChild(footer);
1951		dialog.appendChild(policyFooter);
1952		return dialog;
1953	}
1954
1955	function privacy_cb_openModal() {
1956		var dialog = document.getElementById('di-cookie-pref-dialog');
1957		if (dialog) dialog.showModal();
1958	}
1959
1960	// ─── banner ────────────────────────────────────────────────────────────────
1961
1962	function privacy_cb_createBanner() {
1963		var wrap = privacy_cb_el('div', { id: 'di-consent-banner' });
1964		var inner = privacy_cb_el('div', { className: 'di-cb__inner' });
1965		var p = privacy_cb_el('p', { className: 'di-cb__text' });
1966
1967		var match = privacy_config.bannerText.match(/^([\s\S]*?)\[([^\]]+)\]([\s\S]*)$/);
1968		if (match) {
1969			p.appendChild(document.createTextNode(match[1]));
1970			p.appendChild(privacy_cb_el('a', { className: 'di-cb__learn', href: privacy_config.privacyPolicyUrl, textContent: match[2] }));
1971			p.appendChild(document.createTextNode(match[3]));
1972		} else {
1973			p.textContent = privacy_config.bannerText;
1974		}
1975
1976		var actions = privacy_cb_el('div', { className: 'di-cb__actions' });
1977		var secondary = privacy_config.variant === 'manage'
1978			? privacy_cb_el('button', { className: 'di-cb__btn di-cb__btn--secondary', type: 'button', textContent: privacy_config.manageLabel })
1979			: privacy_cb_el('button', { className: 'di-cb__btn di-cb__btn--secondary', type: 'button', textContent: privacy_config.declineLabel });
1980		if (privacy_config.variant !== 'manage') secondary.setAttribute('data-event', 'consent_reject');
1981		var acceptBtn = privacy_cb_el('button', { className: 'di-cb__btn di-cb__btn--primary', type: 'button', textContent: privacy_config.acceptLabel });
1982		acceptBtn.setAttribute('data-event', 'consent_accept');
1983
1984		secondary.addEventListener('click', privacy_config.variant === 'manage' ? privacy_cb_openModal : privacy_cb_reject);
1985		acceptBtn.addEventListener('click', privacy_cb_accept);
1986
1987		actions.appendChild(secondary);
1988		actions.appendChild(acceptBtn);
1989		inner.appendChild(p);
1990		inner.appendChild(actions);
1991		wrap.appendChild(inner);
1992		return wrap;
1993	}
1994
1995	// ─── init ──────────────────────────────────────────────────────────────────
1996
1997	function privacy_cb_init() {
1998		privacy_config = Object.assign({}, privacy_config_defaults, window.privacy_config || {});
1999		if (window.privacy_allow.disabled === true) return;
2000		if (!window.DiPrivacy || window.DiPrivacy.consent !== 'pending' || window.DiPrivacy.gpcEnabled) return;
2001		privacy_cb_injectStyles();
2002		document.body.appendChild(privacy_cb_createDialog());
2003		var banner = privacy_cb_createBanner();
2004		banner.style.setProperty('--di-cb-primary', privacy_config.primaryColor);
2005		banner.style.setProperty('--di-cb-bg', privacy_config.bgColor);
2006		banner.style.setProperty('--di-cb-text', privacy_config.textColor);
2007		banner.style.setProperty('--di-cb-font', privacy_config.fontFamily);
2008		banner.style.setProperty('--di-cb-radius', privacy_config.buttonRadius);
2009		document.body.appendChild(banner);
2010	}
2011
2012	document.readyState === 'loading'
2013		? document.addEventListener('DOMContentLoaded', privacy_cb_init)
2014		: privacy_cb_init();
2015
2016})();

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.