1 2(function(){ 3 4 // Create a session join key early so sync boot can include it 5 var __qaxal_client_session_id = (function(){ 6 try { 7 var key = "_sid"; 8 var legacyKey = "_qaxal_sid"; 9 10 // 1. Read all sources 11 var cookieVal = (document.cookie.match(new RegExp('(^|;)\s*' + key + '\s*=\s*([^;]+)')) || [])[2]; 12 var storageVal = sessionStorage.getItem(key); 13 var legacyVal = sessionStorage.getItem(legacyKey); 14 15 // 2. Resolve Single Source of Truth (Cookie > Storage > Legacy > New) 16 var sid = cookieVal || storageVal || legacyVal || 17 (crypto.randomUUID ? crypto.randomUUID() : (Date.now()+"."+Math.random().toString(16).slice(2))); 18 19 // 3. Sync EVERYWHERE 20 sessionStorage.setItem(key, sid); 21 22 // Calculate root domain for cookie 23 var parts = location.hostname.split('.'); 24 var rootDomain = parts.length >= 2 ? "." + parts.slice(-2).join('.') : ""; 25 var domainAttr = rootDomain ? "; domain=" + rootDomain : ""; 26 27 // Set _sid cookie (Lax, Secure, Root Domain) 28 document.cookie = key + "=" + sid + "; path=/; SameSite=Lax; Secure" + domainAttr; 29 30 // 4. Cleanup Legacy 31 if (legacyVal) sessionStorage.removeItem(legacyKey); 32 33 return sid; 34 } catch(e) { 35 return null; 36 } 37 })(); 38 39 // EARLY PROXY INTERCEPTOR INSTALL 40 // Ensures first analytics hits are routed via /k/:token?p=<base64> 41 try { 42 if ("44sf49pu" && "44sf49pu".length > 0) { 43 setupProxyInterceptors({ 44 container_token: "44sf49pu", 45 sgtm_url: location.origin, 46 base_path: "" 47 }); 48 } 49 } catch (e) { 50 // fail silently â never block page execution 51 } 52 function fireRestore(containerToken, baseUrl) { 53 if (!containerToken) return Promise.resolve(); 54 55 // Consent signal for the server-set _fpid gate. The page URL almost never 56 // carries gcs, so relying on it alone meant the router fell back to the 57 // qaxal_consent cookie â which tenants on a third-party CMP (Cookiebot & 58 // co.) do not have, so it defaulted to DENY and the anchor was never set. 59 // consentGcs() reads the live Consent Mode state instead; the URL value 60 // still wins when present. 61 var gcs = ""; 62 try { 63 var m = location.search.match(/[?&]gcs=([^&]+)/); 64 if (m && m[1]) gcs = m[1]; 65 } catch(e) {} 66 if (!gcs) { try { gcs = consentGcs(); } catch(e) {} } 67 68 // Use provided base URL or fallback to relative (which might be wrong if cross-origin) 69 var prefix = baseUrl ? baseUrl : ""; 70 // Remove trailing slash if present 71 if (prefix && prefix.slice(-1) === "/") prefix = prefix.slice(0, -1); 72 73 var url = prefix + "/k/" + containerToken + "/r"; 74 var q = []; 75 if (gcs) q.push("gcs=" + encodeURIComponent(gcs)); 76 // Hand back the localStorage mirror so the server can re-adopt the same 77 // anchor after Safari evicted the cookie (LS outlives cookie eviction). 78 try { 79 var lsFp = readFpid(); 80 if (lsFp && lsFp.indexOf("FPID2.") !== 0) q.push("fp=" + encodeURIComponent(lsFp)); 81 } catch(e) {} 82 if (q.length) url += "?" + q.join("&"); 83 84 if (window.fetch) { 85 return fetch(url, { method: "POST", credentials: "include" }) 86 .then(function(r){ return r.text(); }) // consume body 87 .catch(function(){}); 88 } 89 return Promise.resolve(); 90 } 91 92 function afterBoot(data) { 93 if (!data) return; 94 95 // 1) Persist sgtm url for debugging/visibility 96 if (data.sgtm_url) { 97 window.qaxal_sgtm_url = data.sgtm_url; 98 } 99 100 // 2) dataLayer markers (keeps current behavior) 101 try { 102 window.dataLayer = window.dataLayer || []; 103 if (!window.__qaxal_gtm_bootstrap_done) { 104 window.__qaxal_gtm_bootstrap_done = true; 105 window.dataLayer.push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); 106 } 107 window.dataLayer.push({ 108 event: "qaxal_boot", 109 qaxal_fpid: (data && (data.fpid || data.cid)) || null, 110 qaxal_profile_id: data.profile_id || null, 111 qaxal_server_session_id: data.server_session_id || null, 112 qaxal_client_session_id: __qaxal_client_session_id || null, 113 // Server-side signals (HTTP metadata, no consent needed) 114 qaxal_geo_country: data.geo_country || null, 115 qaxal_geo_city: data.geo_city || null, 116 qaxal_geo_region: data.geo_region || null, 117 qaxal_ua_browser: data.ua_browser || null, 118 qaxal_ua_device: data.ua_device || null, 119 qaxal_ua_os: data.ua_os || null 120 }); 121 } catch(e){} 122 123 // 3) Install proxy interceptors (MUST run even when sync boot ran) 124 try { setupProxyInterceptors(data); } catch(e){} 125 126 // 4) Inject GTM container script (Delayed by Restore) 127 if (!window.__qaxal_container_injected && data.container_token) { 128 window.__qaxal_container_injected = true; 129 130 // Pass sgtm_url to ensure we hit the correct tracking domain, not the site domain 131 fireRestore(data.container_token, data.sgtm_url + (data.base_path || "")).then(function() { 132 try { 133 if (data.container_token) { 134 var s = document.createElement("script"); 135 s.async = true; 136 137 var qs = location.search || ""; 138 var previewSuffix = ""; 139 if ( 140 qs.indexOf("gtm_debug") !== -1 || 141 qs.indexOf("gtm_preview") !== -1 || 142 qs.indexOf("gtm_auth") !== -1 || 143 qs.indexOf("gtm_cookies_win") !== -1 144 ) { 145 previewSuffix = qs; 146 } 147 148 s.src = "https://assets.forbes.cz/c/" + data.container_token + ".js" + previewSuffix + (previewSuffix ? "&" : "?") + "xsid=" + encodeURIComponent(__qaxal_client_session_id || ""); 149 document.head.appendChild(s); 150 } 151 } catch(e){} 152 }); 153 } 154
155 // 5) Late restore: the first boot ran with no _fpid, so cookie-keeper restore 156 // (server-keyed on _fpid) was a no-op. Now that consent minted an _fpid, run 157 // restore exactly once. GTM is NOT re-injected (guarded above). 158 if (window.__qaxal_boot_had_no_fpid && !window.__qaxal_restore_fpid_done 159 && data.container_token && readFpid()) { 160 window.__qaxal_restore_fpid_done = true; 161 fireRestore(data.container_token, data.sgtm_url + (data.base_path || "")); 162 } 163 } 164 165 function getCookie(name){ 166 var m = document.cookie.match(new RegExp("(?:^|;\s*)" + name + "=([^;]+)")); 167 return m ? decodeURIComponent(m[1]) : null; 168 } 169 function setCookie(name, value, options){ 170 document.cookie = name + "=" + encodeURIComponent(value) + ";" + options; 171 } 172 // ===================================================== 173 // Consent-aware FPID (ePrivacy Art 5(3) / SK §109) 174 // FPID is the marketing/attribution anchor. It must NOT be minted or 175 // persisted for a NEW visitor before consent. A RETURNING already-consented 176 // visitor's existing _fpid may still be READ pre-consent (it exists lawfully). 177 // "consent present" mirrors the router: statistics OR marketing granted. 178 // _sid (technical session id) is untouched and never linked to fpid. 179 // ===================================================== 180 var FPID_COOKIE = "_fpid", FPID_LS = "_fpid", FPID_LEGACY = "qaxal_fpid"; 181 182 function qaxalReadConsent(){ 183 try { 184 var m = document.cookie.match(/qaxal_consent=([^;]+)/); 185 if (!m) return null; 186 var val = decodeURIComponent(m[1]); 187 if (val === "granted") return { necessary:true, preferences:true, statistics:true, marketing:true }; 188 if (val === "declined") return { necessary:true, preferences:false, statistics:false, marketing:false }; 189 return JSON.parse(val); // { necessary, preferences, statistics, marketing } 190 } catch(e){ return null; } 191 } 192 // Consent is read from Google Consent Mode â the signal EVERY CMP feeds 193 // (Cookiebot, OneTrust, qaxal, â¦), NOT from the qaxal banner specifically. 194 // This is the same signal (gcs) the router already gates on. Granted if 195 // analytics OR ad storage is granted. qaxal_consent stays only as a fallback 196 // for clients that use the qaxal banner without Consent Mode. 197 function consentState(){ 198 try { 199 var ics = window.google_tag_data && window.google_tag_data.ics && window.google_tag_data.ics.entries; 200 if (ics) { 201 var a = ics.analytics_storage, m = ics.ad_storage; 202 var ag = a && a.update; 203 var mg = m && m.update; 204 var hasUpdate = (a && a.update !== undefined) || (m && m.update !== undefined); 205 var granted = function(v){ return v === true || v === "granted"; }; 206 if (hasUpdate) return (granted(ag) || granted(mg)) ? "granted" : "denied"; 207 } 208 } catch (_) {} 209 var c = qaxalReadConsent(); 210 if (!c) return "pending"; 211 return (c.statistics === true || c.marketing === true) ? "granted" : "denied"; 212 } 213 function consentPresent(){ 214 return consentState() === "granted"; 215 } 216 217 // Live consent state as a Consent Mode "gcs" string, for the server-side gate 218 // on /r. Format matches what the router parses: "G1" + ad_storage + 219 // analytics_storage. Returns "" when no signal exists at all, in which case 220 // the router falls back to the qaxal_consent cookie and otherwise denies. 221 function consentGcs(){ 222 var m = null, a = null; 223 try { 224 var ics = window.google_tag_data && window.google_tag_data.ics && window.google_tag_data.ics.entries; 225 if (ics) { 226 var g = function(e){ 227 if (!e) return null; 228 var v = (e.update !== undefined ? e.update : e.default); 229 if (v === undefined) return null; 230 return (v === true || v === "granted") ? 1 : 0; 231 }; 232 a = g(ics.analytics_storage); m = g(ics.ad_storage); 233 } 234 } catch (_) {} 235 if (m === null && a === null) { 236 var c = qaxalReadConsent(); 237 if (!c) return ""; 238 m = (c.marketing === true) ? 1 : 0; 239 a = (c.statistics === true) ? 1 : 0; 240 } 241 return "G1" + (m === null ? 0 : m) + (a === null ? 0 : a); 242 } 243 244 // READ-ONLY: returns an existing fpid, never mints, never writes. Always lawful. 245 function readFpid(){ 246 var v = getCookie(FPID_COOKIE);
247 if (v && v.indexOf("FPID2.") === 0) v = null; 248 if (!v) { try { v = localStorage.getItem(FPID_LS); } catch(e){} if (v && v.indexOf("FPID2.") === 0) v = null; } 249 if (!v) { try { v = localStorage.getItem(FPID_LEGACY); } catch(e){} } 250 return v || null; 251 } 252 253 // MINT + persist â hard-guarded by consentPresent() (defense in depth). 254 // 255 // The cookie IS written here. A previous version moved the write to the router 256 // (/r) so the anchor would be server-set and escape Safari's 7-day cap on 257 // script-written cookies. That dropped cookie restoration by ~95% overnight 258 // (~145k/day to ~5k/day) because the whole cookie-keeper is keyed on _fpid: 259 // /r fires once, from afterBoot, at a point where a third-party CMP often has 260 // not published Consent Mode yet, so the router saw no consent and set 261 // nothing â and the late-consent path could not recover, since it is gated on 262 // readFpid() being non-null, which it never was without this write. 263 // 264 // The server-side write on /r is kept, but as a REFRESH on top of this one 265 // rather than a replacement: whenever consent does reach the router it re-sets 266 // the same value server-side, which restores the full lifetime. Worst case the 267 // anchor is script-written and capped; best case the server upgrades it. It is 268 // never absent. 269 // 270 // Trade-off taken deliberately: the ITP benefit was never observed in our own 271 // data (no 7-day cliff in the fpid survival curve; a JS-written probe kept its 272 // full expiry in both macOS and iOS Safari), while the restoration loss was 273 // measured and severe. 274 function mintFpid(){ 275 if (!consentPresent()) return null; 276 var parts = location.hostname.split('.'); 277 var rootDomain = parts.length >= 2 ? "." + parts.slice(-2).join('.') : ""; 278 var domainAttr = rootDomain ? "; domain=" + rootDomain : ""; 279 280 var fpid = readFpid(); 281 if (!fpid) { 282 fpid = (crypto.randomUUID ? crypto.randomUUID() : (Date.now()+"."+Math.random().toString(16).slice(2))); 283 } 284 try { localStorage.setItem(FPID_LS, fpid); } catch(e){} 285 try { if (localStorage.getItem(FPID_LEGACY)) localStorage.removeItem(FPID_LEGACY); } catch(e){} 286 287 // Remove any stale HOST-ONLY _fpid first: a host-only cookie (e.g. on 288 // www.forbes.sk) shadows the shared root-domain cookie and makes each 289 // subdomain read a different value â split identity across www â checkout 290 // subdomain. Then set ONE clean root-domain cookie (mirrors how _sid is set), 291 // shared across all *.rootdomain subdomains. No host-only fallback. 292 if (rootDomain) { 293 document.cookie = FPID_COOKIE + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"; 294 } 295 document.cookie = FPID_COOKIE + "=" + encodeURIComponent(fpid) + "; Path=/; Secure; SameSite=Lax; Max-Age=63072000" + domainAttr; 296 return fpid; 297 } 298 299 // Never expose the marketing anchor without consent, even when a stale value 300 // already exists in localStorage or a cookie. 301 function resolveFpidForBoot(){ 302 return consentPresent() ? mintFpid() : null; 303 } 304 305 // Active purge on consent withdrawal: erase the FPID marketing anchor from 306 // cookie (root-domain + host-only) and localStorage. Called only on an 307 // explicit deny (not on "no decision yet"). 308 function purgeFpid(){ 309 try { localStorage.removeItem(FPID_LS); } catch(e){} 310 try { localStorage.removeItem(FPID_LEGACY); } catch(e){} 311 var parts = location.hostname.split('.'); 312 var rootDomain = parts.length >= 2 ? "." + parts.slice(-2).join('.') : ""; 313 var domainAttr = rootDomain ? "; domain=" + rootDomain : ""; 314 document.cookie = FPID_COOKIE + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/" + domainAttr; 315 document.cookie = FPID_COOKIE + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"; 316 document.cookie = FPID_LEGACY + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/" + domainAttr; 317 } 318 319 320 321 322 // ===================================================== 323 // Qaxal Consent Bar (module key: consent_bar) 324 // The banner is the Consent Mode v2 PRODUCER; the consent engine above 325 // (qaxalReadConsent / consentPresent / FPID lifecycle) stays the consumer. 326 // Zero-footprint guarantee: when the module is disabled the serve-time cfg 327 // is null and this block is fully inert â no DOM, no listeners, no cookie 328 // writes, no network calls. 329 // Compliance guardrails, hard-coded: no pre-checked categories, Necessary 330 // locked on, refusal remembered (no re-ask within refusal_ttl_days), 331 // X-close = no consent (disclosed in copy), withdrawal via persistent 332 // widget or window.QaxalConsent.open(), layer 2 ALWAYS carries Reject all. 333 // Layer 1 shows equal-prominence Accept/Reject by default; the tenant can 334 // opt out with show_reject_all:false (their legal call â documented as 335 // failing the CNIL/AEPD/Garante/DSK equal-prominence baseline). 336 // custom_ui:true = headless mode: no built-in UI at all, the tenant renders 337 // their own against the window.QaxalConsent API + qaxal:consent-* events; 338 // cookie/Consent Mode/FPID plumbing stays ours. 339 // ===================================================== 340 var __qaxal_consent_cfg = null; 341 (function(){ 342 var cfg = __qaxal_consent_cfg; 343 if (!cfg || !cfg._enabled) return;
344 if (cfg._geo_applies === false) return; 345 346 var VER = cfg.version || 1; 347 var CUSTOM_UI = cfg.custom_ui === true; 348 var isPreview = location.search.indexOf('_qaxal_consent_preview') !== -1; 349 var TTL_GRANT_S = (cfg.consent_ttl_days || 365) * 86400; 350 var TTL_DENY_S = (cfg.refusal_ttl_days || 182) * 86400; 351 352 var I18N = { 353 en: { title:'We value your privacy', text:'We use necessary cookies to run this site. With your consent we also use cookies to measure traffic and personalize marketing. You can change your choice at any time.', accept:'Accept all', reject:'Reject all', settings:'Settings', save:'Save selection', back:'Back', policy:'Privacy policy', widget:'Cookie settings', close_note:'Closing keeps optional cookies off.', cat_necessary:'Necessary', cat_necessary_d:'Required for the site to work (security, basic features, storing this choice). Always on.', cat_preferences:'Preferences', cat_preferences_d:'Remembers choices like language or region.', cat_statistics:'Statistics', cat_statistics_d:'Anonymous usage measurement that helps us improve the site.', cat_marketing:'Marketing', cat_marketing_d:'Used to measure and personalize advertising.' }, 354 sk: { title:'Vážime si vaÅ¡e súkromie', text:'Na prevádzku stránky použÃvame nevyhnutné cookies. S vaÅ¡Ãm súhlasom ich použÃvame aj na meranie návÅ¡tevnosti a personalizáciu marketingu. Svoju voľbu môžete kedykoľvek zmeniÅ¥.', accept:'PrijaÅ¥ vÅ¡etko', reject:'OdmietnuÅ¥ vÅ¡etko', settings:'Nastavenia', save:'UložiÅ¥ výber', back:'Späť', policy:'Zásady ochrany osobných údajov', widget:'Nastavenia cookies', close_note:'ZatvorenÃm ostanú voliteľné cookies vypnuté.', cat_necessary:'Nevyhnutné', cat_necessary_d:'Potrebné pre fungovanie stránky (bezpeÄnosÅ¥, základné funkcie, uloženie tejto voľby). Vždy zapnuté.', cat_preferences:'Preferencie', cat_preferences_d:'Zapamätanie volieb ako jazyk alebo región.', cat_statistics:'Å tatistika', cat_statistics_d:'Anonymné meranie návÅ¡tevnosti, ktoré nám pomáha stránku zlepÅ¡ovaÅ¥.', cat_marketing:'Marketing', cat_marketing_d:'Meranie a personalizácia reklamy.' }, 355 cs: { title:'VážÃme si vaÅ¡eho soukromÃ', text:'K provozu stránky použÃváme nezbytné cookies. S vaÅ¡Ãm souhlasem je použÃváme i k mÄÅenà návÅ¡tÄvnosti a personalizaci marketingu. Svou volbu můžete kdykoli zmÄnit.', accept:'PÅijmout vÅ¡e', reject:'OdmÃtnout vÅ¡e', settings:'NastavenÃ', save:'Uložit výbÄr', back:'ZpÄt', policy:'Zásady ochrany osobnÃch údajů', widget:'Nastavenà cookies', close_note:'ZavÅenÃm zůstanou volitelné cookies vypnuté.', cat_necessary:'Nezbytné', cat_necessary_d:'PotÅebné pro fungovánà stránky (bezpeÄnost, základnà funkce, uloženà této volby). Vždy zapnuto.', cat_preferences:'Preference', cat_preferences_d:'Zapamatovánà voleb jako jazyk nebo region.', cat_statistics:'Statistika', cat_statistics_d:'Anonymnà mÄÅenà návÅ¡tÄvnosti, které nám pomáhá web zlepÅ¡ovat.', cat_marketing:'Marketing', cat_marketing_d:'MÄÅenà a personalizace reklamy.' }, 356 de: { title:'Wir respektieren Ihre Privatsphäre', text:'Wir verwenden notwendige Cookies für den Betrieb der Website. Mit Ihrer Einwilligung nutzen wir Cookies auch zur Reichweitenmessung und Marketing-Personalisierung. Sie können Ihre Wahl jederzeit ändern.', accept:'Alle akzeptieren', reject:'Alle ablehnen', settings:'Einstellungen', save:'Auswahl speichern', back:'Zurück', policy:'Datenschutzerklärung', widget:'Cookie-Einstellungen', close_note:'Beim SchlieÃen bleiben optionale Cookies deaktiviert.', cat_necessary:'Notwendig', cat_necessary_d:'Erforderlich für den Betrieb der Website (Sicherheit, Grundfunktionen, Speicherung dieser Wahl). Immer aktiv.', cat_preferences:'Präferenzen', cat_preferences_d:'Speichert Einstellungen wie Sprache oder Region.', cat_statistics:'Statistik', cat_statistics_d:'Anonyme Nutzungsmessung zur Verbesserung der Website.', cat_marketing:'Marketing', cat_marketing_d:'Messung und Personalisierung von Werbung.' } 357 }; 358 function pickLang(){ 359 var avail = {}; 360 var k; 361 for (k in I18N) avail[k] = 1; 362 if (cfg.languages) { for (k in cfg.languages) avail[k] = 1; } 363 var cands = [];
364 try { if (document.documentElement.lang) cands.push(document.documentElement.lang.slice(0,2).toLowerCase()); } catch(e){} 365 try { if (navigator.language) cands.push(navigator.language.slice(0,2).toLowerCase()); } catch(e){} 366 for (var i = 0; i < cands.length; i++) if (avail[cands[i]]) return cands[i]; 367 return cfg.default_language && avail[cfg.default_language] ? cfg.default_language : 'en'; 368 } 369 var LANG = pickLang(); 370 function t(key){ 371 var ov = cfg.languages && cfg.languages[LANG]; 372 if (ov && typeof ov[key] === 'string' && ov[key]) return ov[key]; 373 var base = I18N[LANG] || I18N.en; 374 return base[key] || I18N.en[key] || key; 375 } 376 377 var OPTIONAL_CATS = ['preferences','statistics','marketing'].filter(function(c){ 378 return !(cfg.categories && cfg.categories[c] && cfg.categories[c].enabled === false); 379 }); 380 381 function gtagPush(){ (window.dataLayer = window.dataLayer || []).push(arguments); } 382 // DOM events for custom_ui builds (and anyone else listening): 383 // qaxal:consent-prompt (a choice is required) / qaxal:consent-change 384 // (a choice was applied). Distinct from the dataLayer event 385 // qaxal_consent_update, which stays for GTM triggers. 386 function fireEvent(name, detail){ 387 try { document.dispatchEvent(new CustomEvent(name, { detail: detail })); } catch(e){} 388 } 389 function signalsFor(c){ 390 var g = function(b){ return b ? 'granted' : 'denied'; }; 391 return { 392 ad_storage: g(c.marketing), ad_user_data: g(c.marketing), ad_personalization: g(c.marketing), 393 analytics_storage: g(c.statistics), 394 functionality_storage: g(c.preferences), personalization_storage: g(c.preferences), 395 security_storage: 'granted' 396 }; 397 } 398 function readStored(){ 399 var c = qaxalReadConsent(); 400 return (c && typeof c === 'object') ? c : null; 401 } 402 function rootDomainAttr(){ 403 var parts = location.hostname.split('.'); 404 var rd = parts.length >= 2 ? '.' + parts.slice(-2).join('.') : ''; 405 return rd ? '; domain=' + rd : ''; 406 } 407 408 // ---- Consent Mode v2 producer ---- 409 // Defaults MUST precede the GTM container. The loader injects GTM only 410 // after /boot resolves, so this synchronous block always runs first. 411 var stored = readStored(); 412 var dflt = signalsFor({ preferences:false, statistics:false, marketing:false }); 413 dflt.wait_for_update = (cfg.consent_mode && cfg.consent_mode.wait_for_update_ms) || 500; 414 gtagPush('consent', 'default', dflt); 415 if (cfg.consent_mode && cfg.consent_mode.url_passthrough) gtagPush('set', 'url_passthrough', true); 416 if (!(cfg.consent_mode && cfg.consent_mode.ads_data_redaction === false)) gtagPush('set', 'ads_data_redaction', true); 417 if (stored) gtagPush('consent', 'update', signalsFor(stored)); 418 419 function writeCookie(c){ 420 var payload = { necessary:true, preferences:!!c.preferences, statistics:!!c.statistics, marketing:!!c.marketing, ts: Date.now(), v: VER }; 421 var anyGrant = payload.preferences || payload.statistics || payload.marketing; 422 var ttl = anyGrant ? TTL_GRANT_S : TTL_DENY_S; 423 document.cookie = 'qaxal_consent=' + encodeURIComponent(JSON.stringify(payload)) + '; Path=/; Max-Age=' + ttl + '; SameSite=Lax; Secure' + rootDomainAttr(); 424 return payload; 425 } 426 // HTTP re-set of the same cookie: server-set first-party cookies keep their 427 // full Max-Age under Safari ITP (JS-set are capped at 7 days). This request 428 // is also the future consent-event log hook (see recordConsentEvent()). 429 function persistServerSide(payload){ 430 try { 431 fetch('https://assets.forbes.cz/qaxal/consent', { 432 method: 'POST', keepalive: true, credentials: 'include', 433 headers: { 'content-type': 'application/json' }, 434 body: JSON.stringify({ loader_token: '2lv7956f', sid: getClientSessionId(), consent: payload, lang: LANG }) 435 }).catch(function(){}); 436 } catch(e){} 437 } 438 439 // ---- UI ---- 440 var host = null, shadowRoot = null, prevFocus = null; 441 function esc(fn){ return function(ev){ try { fn(ev); } catch(e){} }; } 442 function el(tag, cls, text){ 443 var n = document.createElement(tag); 444 if (cls) n.className = cls; 445 if (text) n.textContent = text; // textContent ONLY â tenant strings are never parsed as HTML 446 return n; 447 } 448 function hexA(hex, a){ 449 var h = String(hex || '').replace('#', ''); 450 if (h.length === 3) h = h.charAt(0)+h.charAt(0)+h.charAt(1)+h.charAt(1)+h.charAt(2)+h.charAt(2); 451 var n = parseInt(h, 16); 452 if (isNaN(n) || h.length !== 6) return 'rgba(39,10,255,' + a + ')'; 453 return 'rgba(' + ((n>>16)&255) + ',' + ((n>>8)&255) + ',' + (n&255) + ',' + a + ')'; 454 } 455 function baseCss(){ 456 var th = cfg.theme || {}; 457 var accent = th.accent || '#270aff'; 458 var bg = th.bg || '#ffffff'; 459 var text = th.text || '#111318';
460 var radius = (th.btn_radius_px != null ? th.btn_radius_px : 12) + 'px'; 461 var panelR = (th.panel_radius_px != null ? th.panel_radius_px : 20) + 'px'; 462 var shadowOn = th.shadow !== false; 463 var panelShadow = shadowOn ? '0 24px 64px -16px rgba(8,10,24,.28),0 4px 16px rgba(8,10,24,.08)' : 'none'; 464 var widgetShadow = shadowOn ? '0 4px 20px rgba(8,10,24,.14)' : 'none'; 465 var widgetShadowHover = shadowOn ? '0 6px 24px rgba(8,10,24,.22)' : 'none'; 466 var font = th.font_family || 'system-ui, -apple-system, Segoe UI, Roboto, sans-serif'; 467 return ':host{all:initial}' + 468 '.qcb-wrap{position:fixed;z-index:2147483646;font-family:' + font + ';color:' + text + ';line-height:1.5;font-size:14px;-webkit-font-smoothing:antialiased}' + 469 '.qcb-panel{position:relative;background:' + bg + ';box-shadow:' + panelShadow + ';border:1px solid ' + hexA(text, .06) + ';border-radius:' + panelR + ';padding:24px;box-sizing:border-box;max-height:min(85vh,720px);overflow-y:auto}' + 470 '.qcb-dialog{inset:0;display:flex;align-items:center;justify-content:center;padding:16px}' + 471 '.qcb-dialog .qcb-panel{max-width:440px;width:100%}' + 472 '.qcb-overlay{position:fixed;inset:0;z-index:2147483645;background:rgba(10,12,24,.45);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}' + 473 '.qcb-corner-left{left:20px;bottom:20px;max-width:380px}' + 474 '.qcb-corner-right{right:20px;bottom:20px;max-width:380px}' + 475 '.qcb-bar-top{top:0;left:0;right:0}.qcb-bar-top .qcb-panel{border-radius:0 0 ' + panelR + ' ' + panelR + '}' + 476 '.qcb-bar-bottom{bottom:0;left:0;right:0}.qcb-bar-bottom .qcb-panel{border-radius:' + panelR + ' ' + panelR + ' 0 0}' + 477 '.qcb-bar-top .qcb-panel,.qcb-bar-bottom .qcb-panel{max-width:none;display:flex;flex-wrap:wrap;gap:16px;align-items:center;justify-content:space-between}' + 478 '.qcb-bar-top .qcb-copy,.qcb-bar-bottom .qcb-copy{flex:1 1 320px;min-width:260px}' + 479 '.qcb-bar-top .qcb-btns,.qcb-bar-bottom .qcb-btns{margin-top:0;flex:0 1 auto}' + 480 '.qcb-title{font-weight:700;font-size:16px;margin:0 0 8px;letter-spacing:-.01em;padding-right:36px}' + 481 '.qcb-text{margin:0;font-size:14px;color:' + hexA(text, .75) + '}' + 482 '.qcb-plink{color:' + accent + ';text-decoration:none;font-weight:500;cursor:pointer}' + 483 '.qcb-plink:hover{text-decoration:underline}' + 484 '.qcb-btns{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px}' + 485 '.qcb-btn{cursor:pointer;font:inherit;font-size:14px;font-weight:600;line-height:1.25;padding:11px 12px;border-radius:' + radius + ';border:none;flex:1 1 calc(50% - 5px);min-width:0;text-align:center;transition:filter .15s ease,background-color .15s ease,transform .06s ease}' + 486 '.qcb-btns .qcb-btn-ghost{flex:1 1 100%}' + 487 '.qcb-btns-noreject .qcb-btn-ghost{flex:1 1 calc(50% - 5px)}' + 488 '.qcb-bar-top .qcb-btn,.qcb-bar-bottom .qcb-btn{flex:0 1 auto;min-width:150px}' + 489 '.qcb-bar-top .qcb-btns .qcb-btn-ghost,.qcb-bar-bottom .qcb-btns .qcb-btn-ghost{flex:0 1 auto}' + 490 '.qcb-btn:active{transform:scale(.98)}' + 491 '.qcb-btn-primary{background:' + accent + ';color:#fff}' + 492 '.qcb-btn-primary:hover{filter:brightness(1.12)}' + 493 '.qcb-btn-ghost{background:' + hexA(accent, .08) + ';color:' + accent + '}' + 494 '.qcb-btn-ghost:hover{background:' + hexA(accent, .14) + '}' + 495 '.qcb-btn:focus-visible,.qcb-plink:focus-visible,.qcb-x:focus-visible,.qcb-widget:focus-visible{outline:3px solid ' + hexA(accent, .5) + ';outline-offset:2px}' + 496 '.qcb-x{position:absolute;top:14px;right:14px;width:32px;height:32px;display:flex;align-items:center;justify-content:center;background:' + hexA(text, .06) + ';border:none;border-radius:999px;font-size:15px;line-height:1;cursor:pointer;color:' + text + ';opacity:.75;transition:opacity .15s ease}' + 497 '.qcb-x:hover{opacity:1}' + 498 '.qcb-cats{margin-top:14px}' + 499 '.qcb-row{display:flex;gap:14px;align-items:center;justify-content:space-between;padding:13px 0;border-top:1px solid ' + hexA(text, .07) + '}' + 500 '.qcb-row:first-child{border-top:none}' + 501 '.qcb-cat-t{font-weight:600;margin:0;font-size:14px}' + 502 '.qcb-cat-d{margin:2px 0 0;font-size:12.5px;color:' + hexA(text, .6) + '}' + 503 '.qcb-sw{position:relative;flex:none;width:44px;height:26px;
503display:inline-block}' + 504 '.qcb-sw input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}' + 505 '.qcb-sw input:disabled{cursor:default}' + 506 '.qcb-knob{position:absolute;inset:0;background:' + hexA(text, .18) + ';border-radius:999px;transition:background-color .18s ease;pointer-events:none}' + 507 '.qcb-knob:after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;background:#fff;border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,.25);transition:transform .18s ease}' + 508 '.qcb-sw input:checked + .qcb-knob{background:' + accent + '}' + 509 '.qcb-sw input:checked + .qcb-knob:after{transform:translateX(18px)}' + 510 '.qcb-sw input:disabled + .qcb-knob{opacity:.45}' + 511 '.qcb-sw input:focus-visible + .qcb-knob{outline:3px solid ' + hexA(accent, .5) + ';outline-offset:2px}' + 512 '.qcb-note{font-size:12px;color:' + hexA(text, .5) + ';margin:12px 0 0}' + 513 '.qcb-foot{font-size:12.5px;margin:12px 0 0}' + 514 '.qcb-foot .qcb-plink{font-weight:500}' + 515 '.qcb-bar-top .qcb-foot,.qcb-bar-bottom .qcb-foot,.qcb-bar-top .qcb-note,.qcb-bar-bottom .qcb-note{margin-top:8px}' + 516 '.qcb-widget{position:fixed;z-index:2147483646;width:48px;height:48px;display:flex;align-items:center;justify-content:center;background:' + bg + ';color:' + text + ';border:1px solid ' + hexA(text, .12) + ';border-radius:999px;padding:0;font-family:' + font + ';cursor:pointer;box-shadow:' + widgetShadow + ';transition:box-shadow .15s ease,transform .1s ease}' + 517 '.qcb-widget:hover{box-shadow:' + widgetShadowHover + ';transform:scale(1.05)}' + 518 '.qcb-widget svg{display:block}' + 519 '.qcb-widget-bl{left:20px;bottom:20px}' + 520 '.qcb-widget-br{right:20px;bottom:20px}' + 521 '@media (max-width:560px){.qcb-corner-left,.qcb-corner-right{left:12px;right:12px;bottom:12px;max-width:none}.qcb-btns{flex-direction:column}.qcb-bar-top .qcb-btns,.qcb-bar-bottom .qcb-btns{flex:1 1 100%}}' + 522 '@media (prefers-reduced-motion:no-preference){.qcb-panel{animation:qcbIn .24s cubic-bezier(.16,1,.3,1)}.qcb-overlay{animation:qcbFade .2s ease-out}}' + 523 '@keyframes qcbIn{from{opacity:0;transform:translateY(14px) scale(.98)}to{opacity:1;transform:none}}' + 524 '@keyframes qcbFade{from{opacity:0}to{opacity:1}}'; 525 } 526 function mountHost(){ 527 if (host) return shadowRoot; 528 host = document.createElement('div'); 529 host.id = 'qaxal-consent-host'; 530 shadowRoot = host.attachShadow ? host.attachShadow({ mode: 'open' }) : host; 531 var style = document.createElement('style'); 532 style.textContent = baseCss(); 533 shadowRoot.appendChild(style); 534 if (cfg.custom_css) { 535 var custom = document.createElement('style'); 536 custom.textContent = String(cfg.custom_css); 537 shadowRoot.appendChild(custom); 538 } 539 document.documentElement.appendChild(host); 540 return shadowRoot; 541 } 542 function clearUi(){ 543 if (!shadowRoot) return; 544 var keep = []; 545 for (var i = 0; i < shadowRoot.children.length; i++) { 546 var n = shadowRoot.children[i]; 547 if (n.tagName === 'STYLE') keep.push(n); 548 } 549 while (shadowRoot.firstChild) shadowRoot.removeChild(shadowRoot.firstChild); 550 for (var j = 0; j < keep.length; j++) shadowRoot.appendChild(keep[j]); 551 } 552 function removeUi(){ 553 if (host && host.parentNode) host.parentNode.removeChild(host); 554 host = null; shadowRoot = null; 555 if (prevFocus && prevFocus.focus) { try { prevFocus.focus(); } catch(e){} prevFocus = null; } 556 } 557 function trapFocus(panel, ev){ 558 if (ev.key !== 'Tab') return; 559 var f = panel.querySelectorAll('button, a[href], input:not([disabled])'); 560 if (!f.length) return; 561 var first = f[0], last = f[f.length - 1]; 562 if (ev.shiftKey && shadowRoot.activeElement === first) { ev.preventDefault(); last.focus(); } 563 else if (!ev.shiftKey && shadowRoot.activeElement === last) { ev.preventDefault(); first.focus(); } 564 } 565 function layoutClass(){ 566 var l = cfg.layout || 'dialog'; 567 if (l === 'corner_bottom_left') return 'qcb-corner-left'; 568 if (l === 'corner_bottom_right') return 'qcb-corner-right'; 569 if (l === 'bar_top') return 'qcb-bar-top'; 570 if (l === 'bar_bottom') return 'qcb-bar-bottom'; 571 return 'qcb-dialog'; 572 } 573 function policyLink(){ 574 var u = cfg.privacy_policy_url; 575 if (!u || u.indexOf('http://') !== 0 && u.indexOf('https://') !== 0) return null; 576 var a = el('a', 'qcb-plink', t('policy')); 577 a.href = u; a.target = '_blank'; a.rel = 'noopener'; 578 return a; 579 } 580 function dismissNoChoice(){ 581 // X-close semantics: NO consent stored, optional cookies stay off, 582 // re-ask on next page load (session-quiet only).
583 try { sessionStorage.setItem('qcb_dismissed', '1'); } catch(e){} 584 removeUi(); 585 mountWidget(); 586 } 587 function renderLayer(layer){ 588 if (CUSTOM_UI) return; // headless mode never mounts built-in UI 589 var root = mountHost(); 590 clearUi(); 591 var lc = layoutClass(); 592 var isDialog = lc === 'qcb-dialog'; 593 var isBar = lc === 'qcb-bar-top' || lc === 'qcb-bar-bottom'; 594 if (isDialog && cfg.overlay !== false) root.appendChild(el('div', 'qcb-overlay')); 595 var wrap = el('div', 'qcb-wrap ' + lc); 596 var panel = el('div', 'qcb-panel'); 597 panel.style.position = 'relative'; 598 panel.setAttribute('role', isDialog ? 'dialog' : 'region'); 599 if (isDialog) panel.setAttribute('aria-modal', 'true'); 600 panel.setAttribute('aria-label', t('title')); 601 602 if (cfg.show_close_x) { 603 var x = el('button', 'qcb-x', 'Ã'); 604 x.setAttribute('aria-label', 'Close'); 605 x.addEventListener('click', esc(dismissNoChoice)); 606 panel.appendChild(x); 607 } 608 609 var copy = el('div', 'qcb-copy'); 610 copy.appendChild(el('p', 'qcb-title', t('title'))); 611 copy.appendChild(el('p', 'qcb-text', t('text'))); 612 panel.appendChild(copy); 613 614 var btns = el('div', 'qcb-btns'); 615 var cur = stored || {}; 616 617 if (layer === 2) { 618 var boxes = {}; 619 var cats = el('div', 'qcb-cats'); 620 // iOS-style switch rows: label/description left, switch right. 621 var catRow = function(name, desc, checked, disabled){ 622 var row = el('div', 'qcb-row'); 623 var w = el('div'); 624 w.appendChild(el('p', 'qcb-cat-t', name)); 625 w.appendChild(el('p', 'qcb-cat-d', desc)); 626 var sw = el('label', 'qcb-sw'); 627 var cb = document.createElement('input'); 628 cb.type = 'checkbox'; 629 cb.checked = !!checked; // never pre-checked without a prior grant 630 cb.disabled = !!disabled; 631 cb.setAttribute('aria-label', name); 632 sw.appendChild(cb); 633 sw.appendChild(el('span', 'qcb-knob')); 634 row.appendChild(w); row.appendChild(sw); 635 cats.appendChild(row); 636 return cb; 637 }; 638 catRow(t('cat_necessary'), t('cat_necessary_d'), true, true); 639 for (var i = 0; i < OPTIONAL_CATS.length; i++) { 640 boxes[OPTIONAL_CATS[i]] = catRow( 641 t('cat_' + OPTIONAL_CATS[i]), 642 t('cat_' + OPTIONAL_CATS[i] + '_d'), 643 cur[OPTIONAL_CATS[i]] === true, 644 false 645 ); 646 } 647 copy.appendChild(cats); 648 var bReject2 = el('button', 'qcb-btn qcb-btn-primary', t('reject')); 649 bReject2.addEventListener('click', esc(function(){ applyChoice({}); })); 650 var bSave = el('button', 'qcb-btn qcb-btn-ghost', t('save')); 651 bSave.addEventListener('click', esc(function(){ 652 var c = {}; 653 for (var k in boxes) c[k] = !!boxes[k].checked; 654 applyChoice(c); 655 })); 656 var bAccept2 = el('button', 'qcb-btn qcb-btn-primary', t('accept')); 657 bAccept2.addEventListener('click', esc(function(){ 658 var c = {}; 659 for (var j = 0; j < OPTIONAL_CATS.length; j++) c[OPTIONAL_CATS[j]] = true; 660 applyChoice(c); 661 })); 662 // Pair row: Reject + Accept (equal prominence); Save wraps full-width below. 663 btns.appendChild(bReject2); btns.appendChild(bAccept2); btns.appendChild(bSave); 664 } else { 665 // Layer 1: Accept and Reject share the SAME primary style (equal 666 // prominence â CNIL/AEPD/Garante/DSK baseline). Settings is secondary. 667 // show_reject_all:false is a tenant opt-out (non-default): Reject 668 // disappears from layer 1 only â layer 2 always keeps Reject all, so 669 // refusal stays two clicks away â and Settings joins Accept in the 670 // pair row instead of wrapping full-width. 671 var bSettings = el('button', 'qcb-btn qcb-btn-ghost', t('settings')); 672 bSettings.addEventListener('click', esc(function(){ renderLayer(2); })); 673 var bAccept = el('button', 'qcb-btn qcb-btn-primary', t('accept')); 674 bAccept.addEventListener('click', esc(function(){ 675 var c = {}; 676 for (var j = 0; j < OPTIONAL_CATS.length; j++) c[OPTIONAL_CATS[j]] = true; 677 applyChoice(c); 678 })); 679 if (cfg.show_reject_all !== false) { 680 var bReject = el('button', 'qcb-btn qcb-btn-primary', t('reject')); 681 bReject.addEventListener('click', esc(function(){ applyChoice({}); })); 682 // Pair row: Reject + Accept (equal prominence); Settings wraps full-width below. 683 btns.appendChild(bReject); btns.appendChild(bAccept); btns.appendChild(bSettings); 684 } else { 685 btns.className += ' qcb-btns-noreject'; 686 btns.appendChild(bSettings); btns.appendChild(bAccept); 687 } 688 } 689 panel.appendChild(btns); 690 // In bar layouts the panel is a flex ROW, so panel-level children would 691 // float beside the buttons; the note + policy link belong under the copy. 692 var footHost = isBar ? copy : panel; 693 if (cfg.show_close_x) footHost.appendChild(el('p', 'qcb-note', t('close_note'))); 694 var pl = policyLink(); 695 if (pl) { 696 var foot = el('p', 'qcb-foot'); 697 foot.appendChild(pl); 698 footHost.appendChild(foot); 699 } 700 701 wrap.appendChild(panel); 702 root.appendChild(wrap); 703 704 prevFocus = prevFocus || document.activeElement; 705 panel.addEventListener('keydown', esc(function(ev){ 706 if (ev.key === 'Escape' && cfg.show_close_x) dismissNoChoice(); 707 if (isDialog) trapFocus(panel, ev); 708 })); 709 var firstBtn = btns.querySelector('button'); 710 if (firstBtn) { try { firstBtn.focus(); } catch(e){} } 711 } 712 var widgetEl = null; 713 // Static SVG (lucide "cookie" glyph), no config-derived content â the one 714 // deliberate innerHTML in this module. currentColor inherits theme text. 715 var QCB_COOKIE_SVG = '<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
715<path d="M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5"></path><path d="M8.5 8.5v.01"></path><path d="M16 15.5v.01"></path><path d="M12 12v.01"></path><path d="M11 17v.01"></path><path d="M7 14v.01"></path></svg>'; 716 function mountWidget(){ 717 if (CUSTOM_UI || cfg.floating_widget === false || widgetEl) return; 718 var root = mountHostWidget(); 719 var pos = cfg.widget_position === 'bottom_right' ? 'qcb-widget-br' : 'qcb-widget-bl'; 720 var b = el('button', 'qcb-widget ' + pos); 721 b.setAttribute('aria-label', t('widget')); 722 b.setAttribute('title', t('widget')); 723 b.innerHTML = QCB_COOKIE_SVG; 724 b.addEventListener('click', esc(function(){ stored = readStored(); renderLayer(2); })); 725 root.appendChild(b); 726 widgetEl = b; 727 } 728 var widgetHost = null; 729 function mountHostWidget(){ 730 if (widgetHost) return widgetHost.shadowRoot || widgetHost; 731 widgetHost = document.createElement('div'); 732 widgetHost.id = 'qaxal-consent-widget-host'; 733 var r = widgetHost.attachShadow ? widgetHost.attachShadow({ mode: 'open' }) : widgetHost; 734 var style = document.createElement('style'); 735 style.textContent = baseCss(); 736 r.appendChild(style); 737 document.documentElement.appendChild(widgetHost); 738 return r; 739 } 740 function unmountWidget(){ 741 if (widgetHost && widgetHost.parentNode) widgetHost.parentNode.removeChild(widgetHost); 742 widgetHost = null; widgetEl = null; 743 } 744 745 function applyChoice(c){ 746 var payload = writeCookie(c); 747 stored = payload; 748 gtagPush('consent', 'update', signalsFor(payload)); 749 try { window.dataLayer.push({ event: 'qaxal_consent_update', qaxal_consent: payload }); } catch(e){} 750 fireEvent('qaxal:consent-change', { consent: payload }); 751 persistServerSide(payload); 752 // Deterministic same-tick FPID mint/purge + boot re-run (the consent 753 // watch would also catch this, slightly later). 754 try { onConsentMaybeChanged(); } catch(e){} 755 removeUi(); 756 unmountWidget(); 757 mountWidget(); 758 } 759 760 // Public API for SPAs, custom footer links and custom_ui (headless) 761 // builds. Documented in docs/consent-bar.md â treat it as a stable 762 // contract once tenant UIs depend on it. accept/reject/save run the FULL 763 // pipeline (cookie, Consent Mode update, dataLayer event, POST /consent, 764 // FPID mint/purge), so a custom UI only has to render and call in. 765 window.QaxalConsent = { 766 open: function(){ 767 stored = readStored(); 768 if (CUSTOM_UI) { fireEvent('qaxal:consent-prompt', { reason: 'open' }); return; } 769 renderLayer(2); 770 }, 771 getState: function(){ return readStored(); }, 772 shouldPrompt: function(){ return shouldPrompt(); }, 773 getCategories: function(){ return OPTIONAL_CATS.slice(); }, 774 getLanguage: function(){ return LANG; }, 775 getTexts: function(){ 776 var out = {}, k; 777 for (k in I18N.en) out[k] = t(k); 778 return out; 779 }, 780 acceptAll: function(){ 781 var c = {}, j; 782 for (j = 0; j < OPTIONAL_CATS.length; j++) c[OPTIONAL_CATS[j]] = true; 783 applyChoice(c); 784 }, 785 rejectAll: function(){ applyChoice({}); }, 786 save: function(choices){ 787 // Masked to enabled categories: disabled ones can never be granted. 788 var c = {}, j, cat; 789 for (j = 0; j < OPTIONAL_CATS.length; j++) { 790 cat = OPTIONAL_CATS[j]; 791 c[cat] = !!(choices && choices[cat]); 792 } 793 applyChoice(c); 794 }, 795 version: VER, 796 customUi: CUSTOM_UI 797 }; 798 799 function shouldPrompt(){ 800 if (isPreview) return true; 801 var c = readStored(); 802 if (c && c.v === VER) return false; // valid choice for current config 803 if (c && c.v !== VER) return true; // material config change â re-ask 804 try { if (sessionStorage.getItem('qcb_dismissed') === '1') return false; } catch(e){} 805 return true; // no choice yet 806 } 807 function boot(){ 808 if (CUSTOM_UI) { 809 // Headless: never mounts built-in UI. Fire the prompt event for the 810 // tenant's UI; scripts that load after this point must ALSO check 811 // QaxalConsent.shouldPrompt() on their own init, since this event can 812 // fire before their listener attaches. Dismissal memory is the 813 // tenant's job here (the event re-fires every page load until a 814 // choice is stored). 815 if (shouldPrompt()) { 816 fireEvent('qaxal:consent-prompt', { 817 reason: readStored() ? 'version_change' : 'no_choice' 818 }); 819 } 820 return; 821 } 822 if (shouldPrompt()) renderLayer(1); 823 else if (readStored()) mountWidget(); 824 } 825 if (document.body) boot(); 826 else document.addEventListener('DOMContentLoaded', esc(boot)); 827 })(); 828 829 function setupProxyInterceptors(data) { 830 if (!data || !data.sgtm_url || !data.container_token) return; 831 832 (function(){ 833 var containerToken = data.container_token; 834 var proxyOrigin; 835 836 try { 837 proxyOrigin = new URL(data.sgtm_url).origin; 838 } catch (e) { 839 return; 840 } 841 842 // Same-origin path mode: everything the browser sends must be anchored 843 // under this base (e.g. "/metrics") so it hits the forwarder route. 844 // Empty in subdomain mode â all logic below is a no-op (identical to 845 // before). basePath has a leading slash and no trailing slash. 846 var basePath = (data.base_path || ""); 847 if (basePath && basePath.slice(-1) === "/") basePath = basePath.slice(0, -1); 848 // Strip the base off a same-origin pathname to get the canonical DIP 849 // path (what the /k/ payload and match logic expect). Leaves external 850 // (GA) pathnames untouched. 851 function stripBase(p) { 852 if (!basePath) return p; 853 if (p === basePath) return "/"; 854 if (p.indexOf(basePath + "/") === 0) return p.slice(basePath.length); 855 return p; 856 } 857 858 function encodePayload(str) { 859 try { return btoa(str); } catch (e) { return null; } 860 } 861 862 function shouldAttachHeaders(targetUrl){ 863 // IMPORTANT: 864 // Do NOT attach custom headers to /k/ because it triggers CORS preflight. 865 // We'll pass xsid via query param instead. 866 return false;
867 } 868 869 function mergeHeaders(init){ 870 var sig = getSignalHeaders(); 871 init = init || {}; 872 var h = init.headers; 873 874 try { 875 if (!h) { 876 init.headers = sig; 877 return init; 878 } 879 880 if (typeof Headers !== "undefined" && h instanceof Headers) { 881 for (var k in sig) h.set(k, sig[k]); 882 init.headers = h; 883 return init; 884 } 885 886 if (Array.isArray(h)) { 887 for (var k2 in sig) h.push([k2, sig[k2]]); 888 init.headers = h; 889 return init; 890 } 891 892 if (typeof h === "object") { 893 for (var k3 in sig) h[k3] = sig[k3]; 894 init.headers = h; 895 return init; 896 } 897 898 init.headers = sig; 899 return init; 900 } catch(e){ 901 init.headers = sig; 902 return init; 903 } 904 } 905 906 function rewrite(rawUrl) { 907 908 //Logs 909 try { 910 if (window.__QAXAL_DEBUG__) { 911 console.log("[qaxal][rewrite] rawUrl =", rawUrl); 912 } 913 } catch(e){} 914 915 try { 916 var u = new URL(rawUrl, location.href); 917 var hasAb = isBlockingEnvironment(); 918 var hasItp = getItpState(); 919 920 // Treat BOTH proxy origin AND page origin as proxy-eligible 921 var pageOrigin = location.origin; 922 923 // Canonical (base-stripped) path so path-mode collect hits 924 // (/<prefix>/g/collect) still match and the /k/ payload carries 925 // the canonical path the router expects. No-op in subdomain mode. 926 var canonPath = stripBase(u.pathname); 927 var isProxyEligibleCollect = 928 ( 929 u.origin === proxyOrigin || 930 u.origin === pageOrigin || 931 u.hostname === "www.google-analytics.com" || 932 u.hostname === "google-analytics.com" || 933 u.hostname.endsWith(".analytics.google.com") 934 ) && 935 ( 936 canonPath === "/collect" || 937 canonPath.indexOf("/g/collect") === 0 || 938 canonPath.indexOf("/r/collect") === 0 939 ); 940 941 if (isProxyEligibleCollect) { 942 943 // sendBeacon-safe markers 944 if (hasAb) u.searchParams.set("xab", "1"); 945 else u.searchParams.delete("xab"); 946 947 if (hasItp) u.searchParams.set("xitp", "1"); 948 else u.searchParams.delete("xitp"); 949 950 var encodedInternal = encodePayload(canonPath + u.search); 951 952 //logs 953 try { 954 if (window.__QAXAL_DEBUG__) { 955 console.log("[qaxal][rewrite] encoded payload =", encodedInternal); 956 } 957 } catch(e){} 958 959 if (!encodedInternal) return rawUrl; 960 961 var proxy = new URL(basePath + "/k/" + containerToken, proxyOrigin); 962 proxy.searchParams.set("p", encodedInternal); 963 964 // session join key without headers (avoids preflight) 965 if (__qaxal_client_session_id) proxy.searchParams.set("xsid", __qaxal_client_session_id); 966 967 //logs 968 try { 969 if (window.__QAXAL_DEBUG__) { 970 console.log("[qaxal][rewrite] final proxy url =", proxy.toString()); 971 } 972 } catch(e){} 973 974 return proxy.toString(); 975 } 976 977 if (u.hostname === "www.google.com" && u.pathname === "/ccm/collect") { 978 if (hasAb) u.searchParams.set("xab", "1"); 979 else u.searchParams.delete("xab"); 980 981 if (hasItp) u.searchParams.set("xitp", "1"); 982 else u.searchParams.delete("xitp"); 983 984 var encodedFull = encodePayload(u.toString()); 985 if (!encodedFull) return rawUrl; 986 987 var proxy2 = new URL(basePath + "/k/" + containerToken, proxyOrigin); 988 proxy2.searchParams.set("p", encodedFull); 989 990 // session join key without headers (avoids preflight) 991 if (__qaxal_client_session_id) proxy2.searchParams.set("xsid", __qaxal_client_session_id); 992 993 proxy2.__qaxal_ccm = true; 994 return proxy2.toString(); 995 } 996 997 return rawUrl; 998 } catch (e) { 999 return rawUrl; 1000 } 1001 } 1002 1003 if (navigator && navigator.sendBeacon) { 1004 var origSB = navigator.sendBeacon.bind(navigator); 1005 navigator.sendBeacon = function(url, body) { 1006 return origSB(rewrite(url), body); 1007 }; 1008 } 1009 1010 if (window.fetch) { 1011 var origFetch = window.fetch.bind(window); 1012 window.fetch = function(resource, init) { 1013 // IMPORTANT: CCM must remain pure GET (no keepalive, no body) 1014 try { 1015 if (typeof rewritten === "string") { 1016 var uccm = new URL(rewritten, location.href); 1017 if (stripBase(uccm.pathname).indexOf("/k/") === 0 && rewritten.indexOf("ccm/collect") !== -1) { 1018 return origFetch(resource, init); 1019 } 1020 } 1021 } catch(e){} 1022 try { 1023 var rewritten; 1024 1025 function maybeEnableKeepalive(rewrittenUrl, initObj){ 1026 try { 1027 // Reduce "(canceled)" on navigation: allow small POSTs to finish during unload. 1028 // Applies only to our /k/ proxy requests. 1029 var u = new URL(rewrittenUrl, location.href);
1030 if (u.origin === proxyOrigin && stripBase(u.pathname).indexOf("/k/") === 0) { 1031 initObj = initObj || {}; 1032 var m = (initObj.method || "GET").toUpperCase(); 1033 if (m === "POST" && typeof initObj.keepalive === "undefined") { 1034 initObj.keepalive = true; 1035 } 1036 } 1037 } catch(e){} 1038 return initObj; 1039 } 1040 1041 if (typeof resource === "string") { 1042 rewritten = rewrite(resource); 1043 resource = rewritten; 1044 1045 if (shouldAttachHeaders(rewritten)) { 1046 init = mergeHeaders(init); 1047 } 1048 1049 init = maybeEnableKeepalive(rewritten, init); 1050 1051 } else if (resource && resource.url) { 1052 rewritten = rewrite(resource.url); 1053 1054 try { 1055 var req2 = new Request(rewritten, resource); 1056 resource = req2; 1057 } catch(e2){ 1058 resource = rewritten; 1059 } 1060 1061 if (shouldAttachHeaders(rewritten)) { 1062 init = mergeHeaders(init); 1063 } 1064 1065 // If the caller passed a Request object and no init, keepalive was not applied. 1066 // Apply keepalive via init override so POST /k/ can finish during navigation/unload. 1067 init = maybeEnableKeepalive(rewritten, init); 1068 try { 1069 var u3 = new URL(rewritten, location.href); 1070 if (u3.origin === proxyOrigin && stripBase(u3.pathname).indexOf("/k/") === 0) { 1071 var method3 = ((init && init.method) || (resource && resource.method) || "GET").toUpperCase(); 1072 if (method3 === "POST") { 1073 init = init || {}; 1074 if (typeof init.keepalive === "undefined") init.keepalive = true; 1075 } 1076 } 1077 } catch(e3){} 1078 } 1079 } catch(e){} 1080 return origFetch(resource, init); 1081 }; 1082 } 1083 1084 if (window.XMLHttpRequest && XMLHttpRequest.prototype.open) { 1085 var origOpen = XMLHttpRequest.prototype.open; 1086 var origSend = XMLHttpRequest.prototype.send; 1087 1088 XMLHttpRequest.prototype.open = function(method, url) { 1089 var rewritten = url; 1090 try { rewritten = rewrite(url); } catch(e){} 1091 this.__qaxal_url = rewritten; 1092 var args = Array.prototype.slice.call(arguments); 1093 args[1] = rewritten; 1094 return origOpen.apply(this, args); 1095 }; 1096 1097 XMLHttpRequest.prototype.send = function(body) { 1098 try { 1099 if (this.__qaxal_url && shouldAttachHeaders(this.__qaxal_url)) { 1100 var sig = getSignalHeaders(); 1101 for (var k in sig) { 1102 try { this.setRequestHeader(k, sig[k]); } catch(e2){} 1103 } 1104 } 1105 } catch(e){} 1106 return origSend.apply(this, arguments); 1107 }; 1108 } 1109 })(); 1110 } 1111 1112 1113 1114 function getClientSessionId(){ 1115 try{ 1116 var key = "_sid"; 1117 // Try cookie first (server visibility), then storage 1118 var match = document.cookie.match(new RegExp('(^|;)\s*' + key + '\s*=\s*([^;]+)')); 1119 var sid = match ? match[2] : sessionStorage.getItem(key); 1120 1121 if (!sid){ 1122 sid = (crypto.randomUUID ? crypto.randomUUID() : (Date.now()+"."+Math.random().toString(16).slice(2))); 1123 sessionStorage.setItem(key, sid); 1124 } 1125 1126 // Calculate root domain (e.g. .nikub.sk) to allow sharing with data.* subdomains 1127 var parts = location.hostname.split('.'); 1128 var rootDomain = parts.length >= 2 ? "." + parts.slice(-2).join('.') : ""; 1129 var domainAttr = rootDomain ? "; domain=" + rootDomain : ""; 1130 1131 // Cleanup legacy host-only cookie (prevent split session ID) 1132 if (rootDomain) { 1133 document.cookie = key + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"; 1134 } 1135 1136 // Ensure cookie is set (Session cookie, strict, secure, ROOT DOMAIN) 1137 document.cookie = key + "=" + sid + "; path=/; SameSite=Lax; Secure" + domainAttr; 1138 1139 return sid; 1140 }catch(e){ 1141 return null; 1142 } 1143 } 1144 1145 // -------------------------------------------------- 1146 // Session-level signals (AB + ITP) 1147 // - AB sessionStorage key: "ab" 1148 // - ITP sessionStorage key: "itp" 1149 // -------------------------------------------------- 1150 function ssGet(k){ try { return sessionStorage.getItem(k); } catch(e){ return null; } } 1151 function ssSet(k,v){ try { sessionStorage.setItem(k, v); } catch(e){} } 1152 1153 // Lightweight ITP heuristic (safe to run immediately) 1154 function detectItpHeuristic(){ 1155 try { 1156 var ua = navigator.userAgent || ""; 1157 var vendor = navigator.vendor || ""; 1158 var isAppleVendor = vendor.indexOf("Apple") !== -1; 1159 var isSafari = 1160 isAppleVendor && 1161 ua.indexOf("Safari") !== -1 && 1162 ua.indexOf("Chrome") === -1 && 1163 ua.indexOf("CriOS") === -1 && 1164 ua.indexOf("FxiOS") === -1; 1165 1166 var isIOS = /iP(hone|ad|od)/.test(ua); 1167 var isWebKit = (ua.indexOf("AppleWebKit") !== -1) && (ua.indexOf("Gecko") === -1); 1168 1169 var likely = !!(isSafari || (isIOS && isWebKit)); 1170 ssSet("itp", likely ? "1" : "0"); 1171 } catch(e){ 1172 ssSet("itp", "0"); 1173 } 1174 } 1175 1176 function getAbState(){ return ssGet("ab"); } 1177 function setAbState(v){ ssSet("ab", v); } 1178 1179 var __qaxal_abwall = {"level":3,"dialog":{"icon":"sad","title":"ProsÃm, vypnite svoj adblock","body_html":"Práve vÄaka naÅ¡im inzerentom, môžete prezeraÅ¥ náš obsah bez obmedzenÃ, preto by sme vás radi poprosili aby ste vypli svoj adblock.","body_size":13,"button_bg":"#000000","title_size":20,"action_text":"Rozumiem, vypol som blokovanie reklám","icon_accent":"#f5f5f5"},"whitelist_paths":[],"_enabled":false}; 1180 1181 // One-shot beacon to /signal for adblock-wall lifecycle events. 1182 // Fire-and-forget; never throws into caller. 1183 function sendQaxalSignal(eventName) { 1184 try { 1185 var _sig = JSON.stringify({ 1186 loader_token: "2lv7956f", 1187 sid: getClientSessionId(), 1188 url: location.href.slice(0, 512), 1189 event: eventName 1190 }); 1191 var _blob = new Blob([_sig], { type: "application/json" }); 1192 if (navigator.sendBeacon) { 1193 navigator.sendBeacon("https://assets.forbes.cz/signal", _blob); 1194 } else { 1195 fetch("https://assets.forbes.cz/signal", { method: "POST", body: _sig, keepalive: true }).catch(function(){}); 1196 } 1197 } catch(e) {} 1198 } 1199 1200 // Record that the wall was shown to this session â at most once per session. 1201 function signalWallShown() { 1202 if (ssGet("ab_wall_shown_sent") === "1") return; 1203 ssSet("ab_wall_shown_sent", "1"); 1204 sendQaxalSignal("adblock_detected"); 1205 } 1206 1207 function applyAdblockWall() { 1208 var isPreview = location.search.indexOf('_qaxal_abwall_preview') !== -1; 1209 var cfg = __qaxal_abwall; 1210 if (!cfg) { 1211 if (!isPreview) return; 1212 cfg = { level: 1, dialog: {} }; 1213 } 1214 var level = cfg.level || 0; 1215 if (level === 0) return; 1216 1217 // Preview mode bypasses enabled check and adblock detection 1218 // Real users: only show if module is enabled AND adblock detected 1219 if (!isPreview) { 1220 if (!cfg._enabled) return; 1221 if (!isBlockingEnvironment()) return; 1222 } 1223 1224 // Whitelist check 1225 var wl = cfg.whitelist_paths || []; 1226 var p = location.pathname; 1227 for (var i = 0; i < wl.length; i++) { 1228 if (p === wl[i] || p.indexOf(wl[i] + '/') === 0) return; 1229 } 1230 1231 // Level 1 soft dismissal is session-wide, not per-page: once the visitor 1232 // dismisses it, stay quiet for the rest of the session (across subpages). 1233 // Levels 2/3 are "until refreshed" hard walls and are never dismiss-sticky. 1234 if (level === 1 && !isPreview && ssGet('ab_wall_dismissed') === '1') return; 1235 1236 // Record the wall impression for this session (denominator for the
1237 // disable-rate metric). Once-per-session; never fired in preview. 1238 if (!isPreview) signalWallShown(); 1239 1240 var d = cfg.dialog || {}; 1241 var btnColor = d.button_bg || '#270aff'; 1242 1243 // ââ Icon SVGs ââââââââââââââââââââââââââââââââââââââââââââââââ 1244 var ICONS = { 1245 sad: '<svg width="26" height="26" viewBox="0 0 28 28" fill="none"><circle cx="14" cy="14" r="12" stroke="' + btnColor + '" stroke-width="2"/><circle cx="10" cy="11" r="1.5" fill="' + btnColor + '"/><circle cx="18" cy="11" r="1.5" fill="' + btnColor + '"/><path d="M10 19.5c1.2-2.5 6.8-2.5 8 0" stroke="' + btnColor + '" stroke-width="2" stroke-linecap="round"/></svg>', 1246 warning: '<svg width="26" height="24" viewBox="0 0 28 26" fill="none"><path d="M14 2L26.5 24H1.5L14 2Z" stroke="' + btnColor + '" stroke-width="2" stroke-linejoin="round"/><line x1="14" y1="10" x2="14" y2="16" stroke="' + btnColor + '" stroke-width="2.2" stroke-linecap="round"/><circle cx="14" cy="20.5" r="1.3" fill="' + btnColor + '"/></svg>', 1247 lock: '<svg width="20" height="26" viewBox="0 0 22 28" fill="none"><rect x="1" y="12" width="20" height="14" rx="3" stroke="' + btnColor + '" stroke-width="2"/><path d="M6 12V8.5a5.5 5.5 0 0 1 11 0V12" stroke="' + btnColor + '" stroke-width="2" stroke-linecap="round"/><circle cx="11" cy="19" r="2" fill="' + btnColor + '"/></svg>' 1248 }; 1249 1250 var accentColor = d.icon_accent || '#ece9ff'; 1251 var iconKey = d.icon || 'sad'; 1252 var iconSvg = ICONS[iconKey] || ''; 1253 var titleText = d.title || 'Please disable your adblocker'; 1254 var bodyHtml = d.body_html || '<p>We rely on ads to keep this content free. Please disable your adblocker to continue.</p>'; 1255 var btnLabel = d.action_text || (level === 1 ? 'Got it' : "I've disabled my adblocker - refresh"); 1256 var btnR = d.button_radius || '9px'; 1257 var delay = level === 1 && d.trigger_delay_ms > 0 ? d.trigger_delay_ms : 0; 1258 1259 // ââ Banner path (Level 1 only) ââââââââââââââââââââââââââââââââ 1260 if (level === 1 && d.display_style === 'banner') { 1261 var bannerPos = d.banner_position === 'top' ? 'top' : 'bottom'; 1262 var edgeProp = bannerPos === 'top' ? 'top' : 'bottom'; 1263 var slideFn = bannerPos === 'top' ? 'translateY(-100%)' : 'translateY(100%)'; 1264 1265 // Strip HTML tags from body for single-line banner display 1266 var bodyText = ''; 1267 if (d.body_html) { 1268 try { 1269 var _tmp = document.createElement('div'); 1270 _tmp.innerHTML = d.body_html; 1271 bodyText = _tmp.textContent || _tmp.innerText || ''; 1272 } catch(e) { bodyText = d.body_html; } 1273 } 1274 1275 // Inject slide-in animation 1276 var anim = d.entry_animation !== 'none' ? 'none' : 'none'; 1277 var styleEl = document.createElement('style'); 1278 styleEl.textContent = 1279 '@keyframes qaxal-banner-in{from{transform:' + slideFn + ';opacity:0}to{transform:translateY(0);opacity:1}}' + 1280 '#qaxal-abwall-banner{animation:qaxal-banner-in .28s ease}'; 1281 document.head.appendChild(styleEl); 1282 1283 var shadow = bannerPos === 'top' 1284 ? '0 4px 16px rgba(0,0,0,.1)' 1285 : '0 -4px 16px rgba(0,0,0,.1)'; 1286 var borderSide = bannerPos === 'top' ? 'border-bottom' : 'border-top'; 1287 1288 var banner = document.createElement('div'); 1289 banner.id = 'qaxal-abwall-banner'; 1290 banner.setAttribute('style', [ 1291 'position:fixed', 1292 edgeProp + ':0', 1293 'left:0', 1294 'right:0', 1295 'z-index:2147483647', 1296 'display:flex', 1297 'align-items:center', 1298 'gap:12px', 1299 'padding:10px 16px', 1300 'background:' + (d.bg_color || '#fff'), 1301 'color:' + (d.text_color || '#111'), 1302 borderSide + ':1px solid rgba(0,0,0,.08)', 1303 'box-shadow:' + shadow, 1304 'font-family:system-ui,sans-serif', 1305 'box-sizing:border-box' 1306 ].join(';')); 1307 1308 var bHtml = ''; 1309 1310 // Icon circle (smaller for banner) 1311 if (iconKey !== 'none' && iconSvg) { 1312 var smallSvg = iconSvg.replace(/width="d+"/, 'width="18"').replace(/height="d+"/, 'height="18"'); 1313 bHtml += '<div style="width:34px;height:34px;border-radius:50%;background:' + accentColor + ';display:flex;align-items:center;justify-content:center;flex-shrink:0">' + smallSvg + '</div>'; 1314 } 1315 1316 // Text column 1317 bHtml += '<div style="flex:1;min-width:0;overflow:hidden">'; 1318 bHtml += '<div style="font-weight:700;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + titleText + '</div>'; 1319 var bannerBody = bodyText || 'We rely on ads to keep this content free. Please disable your adblocker to continue.'; 1320 bHtml += '<div style="font-size:12px;color:#666;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">' + bannerBody + '</div>'; 1321 bHtml += '</div>'; 1322 1323 // CTA button 1324 bHtml += '<button id="qaxal-abwall-btn" style="' + 1325 'flex-shrink:0;white-space:nowrap;' + 1326 'background:' + btnColor + ';' + 1327 'color:' + (d.button_color || '#fff') + ';' + 1328 'border:none;border-radius:' + btnR + ';' + 1329 'padding:8px 14px;cursor:pointer;font-size:13px;font-weight:600' + 1330 '">' + btnLabel + '</button>'; 1331 1332 // Dismiss à (respects show_close, defaults on for banner) 1333 if (d.show_close !== false) { 1334 bHtml += '<button id="qaxal-abwall-close" style="flex-shrink:0;width:26px;height:26px;border-radius:50%;background:#f4f4f5;border:none;cursor:pointer;font-size:15px;
1334color:#777;line-height:26px;padding:0">Ã</button>'; 1335 } 1336 1337 banner.innerHTML = bHtml; 1338 document.body.appendChild(banner); 1339 1340 function removeBanner() { 1341 ssSet('ab_wall_dismissed', '1'); 1342 if (banner.parentNode) banner.parentNode.removeChild(banner); 1343 } 1344 1345 function openBanner() { 1346 document.getElementById('qaxal-abwall-btn').addEventListener('click', removeBanner); 1347 var closeBtn = document.getElementById('qaxal-abwall-close'); 1348 if (closeBtn) closeBtn.addEventListener('click', removeBanner); 1349 } 1350 1351 if (delay > 0) { setTimeout(openBanner, delay); } 1352 else { openBanner(); } 1353 return; 1354 } 1355 1356 // ââ Dialog path (Level 1 dialog, Level 2, Level 3) âââââââââââ 1357 var styleText = ''; 1358 1359 // ::backdrop (can't be set via inline style) 1360 var backdropStyle = 'background:' + (d.backdrop_color || 'rgba(0,0,0,0.55)'); 1361 if (d.backdrop_blur && level >= 2) { 1362 backdropStyle += ';backdrop-filter:blur(' + (d.blur_intensity || 6) + 'px)'; 1363 } 1364 styleText += '#qaxal-abwall::backdrop{' + backdropStyle + '}'; 1365 1366 // Entry animation 1367 var anim = d.entry_animation || 'fade'; 1368 if (anim === 'fade') { 1369 styleText += '@keyframes qaxal-anim{from{opacity:0}to{opacity:1}}#qaxal-abwall[open]{animation:qaxal-anim .25s ease}'; 1370 } else if (anim === 'slide') { 1371 styleText += '@keyframes qaxal-anim{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}#qaxal-abwall[open]{animation:qaxal-anim .25s ease}'; 1372 } else if (anim === 'scale') { 1373 styleText += '@keyframes qaxal-anim{from{opacity:0;transform:scale(.93)}to{opacity:1;transform:scale(1)}}#qaxal-abwall[open]{animation:qaxal-anim .22s ease}'; 1374 } 1375 1376 var styleEl = document.createElement('style'); 1377 styleEl.textContent = styleText; 1378 document.head.appendChild(styleEl); 1379 1380 // ââ Build <dialog> âââââââââââââââââââââââââââââââââââââââââââ 1381 var placement = d.placement || 'center'; 1382 var margin = placement === 'top' ? '2rem auto auto' : placement === 'bottom' ? 'auto auto 2rem' : 'auto'; 1383 var r = d.border_radius || '14px'; 1384 var shadow = d.drop_shadow !== false ? '0 20px 60px rgba(0,0,0,.28)' : 'none'; 1385 1386 var dialog = document.createElement('dialog'); 1387 dialog.id = 'qaxal-abwall'; 1388 dialog.setAttribute('style', [ 1389 'max-width:' + (d.max_width || '480px'), 1390 'width:calc(100% - 2rem)', 1391 'border-radius:' + r, 1392 'background:' + (d.bg_color || '#fff'), 1393 'color:' + (d.text_color || '#111'), 1394 'border:none', 1395 'padding:' + (d.padding || '28px 24px'), 1396 'margin:' + margin, 1397 'box-shadow:' + shadow, 1398 'text-align:center' 1399 ].join(';')); 1400 1401 var titleSize = (d.title_size || 18) + 'px'; 1402 var bodySize = (d.body_size || 13) + 'px'; 1403 1404 // Wrap content in a position:relative div so the close button's 1405 // position:absolute is scoped to the dialog content, not the viewport. 1406 // (The dialog itself must not have position:relative â showModal() needs 1407 // the UA's position:fixed so the dialog stays in view while scrolling.) 1408 var html = '<div style="position:relative">'; 1409 1410 // Close button (Level 1 + show_close not explicitly disabled) 1411 if (level === 1 && d.show_close !== false) { 1412 html += '<button id="qaxal-abwall-close" style="position:absolute;top:-6px;right:-6px;width:26px;height:26px;border-radius:50%;background:#f4f4f5;border:none;cursor:pointer;font-size:15px;color:#777;line-height:26px;padding:0">Ã</button>'; 1413 } 1414 1415 // Icon circle 1416 if (iconKey !== 'none' && iconSvg) { 1417 html += '<div style="width:52px;height:52px;border-radius:50%;background:' + accentColor + ';margin:0 auto 14px;display:flex;align-items:center;justify-content:center">' + iconSvg + '</div>'; 1418 } 1419 1420 // Title 1421 html += '<h2 style="margin:0 0 8px;font-size:' + titleSize + ';font-weight:700;line-height:1.3">' + titleText + '</h2>'; 1422 1423 // Body 1424 html += '<div style="font-size:' + bodySize + ';color:#666;margin:0 0 20px;line-height:1.55">' + bodyHtml + '</div>'; 1425 1426 // CTA button 1427 html += '<button id="qaxal-abwall-btn" style="' + 1428 'background:' + btnColor + ';' + 1429 'color:' + (d.button_color || '#fff') + ';' + 1430 'border:none;border-radius:' + btnR + ';' + 1431 'padding:.75rem 1rem;cursor:pointer;font-size:15px;font-weight:600;width:100%;display:block' + 1432 '">' + btnLabel + '</button>'; 1433 1434 // Secondary dismiss link (Level 1 only) 1435 if (level === 1 && d.secondary_text) { 1436 html += '<div id="qaxal-abwall-sec" style="margin-top:12px;font-size:13px;color:#aaa;cursor:pointer">' + d.secondary_text + '</div>'; 1437 } 1438 1439 html += '</div>'; 1440 dialog.innerHTML = html; 1441 document.body.appendChild(dialog); 1442 1443 // ââ Open (with optional delay on Level 1) ââââââââââââââââââââ 1444 function openDialog() { 1445 dialog.showModal(); 1446 1447 // Apply Level 3 page blur only after dialog is confirmed open, so a 1448 // failed showModal() never leaves the page stuck in a blurred state. 1449 if (level >= 3) { 1450 var blurEl = document.createElement('style'); 1451 blurEl.textContent = 'body>*:not(#qaxal-abwall){filter:blur(8px);pointer-events:none;user-select:none}'; 1452 document.head.appendChild(blurEl); 1453 } 1454 1455 if (level >= 2) { 1456 document.documentElement.style.overflow = 'hidden'; 1457 document.body.style.overflow = 'hidden'; 1458 dialog.addEventListener('cancel', function(e) { e.preventDefault(); }); 1459 } 1460 1461 document.getElementById('qaxal-abwall-btn').addEventListener('click', function() { 1462 if (level === 1) { ssSet('ab_wall_dismissed', '1'); dialog.close(); } 1463 else { window.location.reload(); } 1464 }); 1465 1466 var closeBtn = document.getElementById('qaxal-abwall-close'); 1467 if (closeBtn) { 1468 closeBtn.addEventListener('click', function() { ssSet('ab_wall_dismissed', '1'); dialog.close(); }); 1469 } 1470 1471 var secEl = document.getElementById('qaxal-abwall-sec'); 1472 if (secEl) { 1473 secEl.addEventListener('click', function() { ssSet('ab_wall_dismissed', '1'); dialog.close(); }); 1474 } 1475 } 1476 1477 if (delay > 0) { setTimeout(openDialog, delay); } 1478 else { openDialog(); } 1479 } 1480 1481 function getItpState(){ 1482 return ssGet("itp") === "1"; 1483 } 1484 1485 // NOTE: Added dummy function to prevent reference errors during probe completion 1486 function reportSessionDetection() {} 1487 1488 // -------------------------------------------------- 1489 // AB detection stack (sequential): 1490 // -------------------------------------------------- 1491 function runBlockednessTestAsync(){ 1492 var cur = getAbState(); 1493 var wallActive = __qaxal_abwall && (__qaxal_abwall.level || 0) > 0; 1494 1495 // When wall is active, always re-probe so adblock state is fresh on every load. 1496 // When wall is inactive, use the cached state to skip probes (existing behaviour).
1497 if (!wallActive && (cur === "1" || cur === "0")) { return; } 1498 1499 // If state was already resolved this session, don't re-fire session detection. 1500 var skipDetectionReport = (cur === "1" || cur === "0"); 1501 1502 // Capture state before resetting â used to detect 1â0 recovery transition. 1503 var prevAb = getAbState(); 1504 setAbState("p"); 1505 1506 // Fire a one-shot beacon to /signal when a visitor transitions from adblock=1 to adblock=0. 1507 // sessionStorage flag ensures at most one signal per browser session. 1508 function maybeSignalRecovery() { 1509 if (prevAb !== "1" || ssGet("ab_recovery_sent") === "1" || !__qaxal_abwall || !__qaxal_abwall._enabled) return; 1510 ssSet("ab_recovery_sent", "1"); 1511 sendQaxalSignal("adblock_disabled"); 1512 } 1513 1514 function cacheBust(u){ 1515 try { 1516 var x = new URL(u, location.href); 1517 x.searchParams.set("_qaxal_ab", String(Date.now()) + "_" + Math.random().toString(16).slice(2)); 1518 return x.toString(); 1519 } catch(e){ 1520 var sep = (u.indexOf("?") === -1) ? "?" : "&"; 1521 return u + sep + "_qaxal_ab=" + (Date.now() + "_" + Math.random().toString(16).slice(2)); 1522 } 1523 } 1524 1525 function probe(url){ 1526 try { 1527 return fetch(cacheBust(url), { 1528 method: "GET", 1529 mode: "no-cors", 1530 cache: "no-store", 1531 credentials: "omit" 1532 }).then( 1533 function(){ return true; }, 1534 function(){ return false; } 1535 ); 1536 } catch(e){ 1537 return Promise.resolve(true); 1538 } 1539 } 1540 1541 var firstPartyBait = location.origin + "/ads.js"; 1542 var googleBait = "https://www.googletagmanager.com/gtag/js?id=G-XXXX"; 1543 var metaBait = "https://connect.facebook.net/en_US/fbevents.js"; 1544 1545 probe(firstPartyBait).then(function(ok1){ 1546 if (!ok1) { setAbState("1"); applyAdblockWall(); if (!skipDetectionReport) reportSessionDetection(); return; } 1547 return probe(googleBait).then(function(ok2){ 1548 if (!ok2) { setAbState("1"); applyAdblockWall(); if (!skipDetectionReport) reportSessionDetection(); return; } 1549 return probe(metaBait).then(function(ok3){ 1550 if (ok3) { 1551 setAbState("0"); 1552 maybeSignalRecovery(); 1553 } else { 1554 setAbState("1"); 1555 applyAdblockWall(); 1556 } 1557 if (!skipDetectionReport) reportSessionDetection(); 1558 }); 1559 }); 1560 }).catch(function(){ 1561 setAbState("0"); 1562 maybeSignalRecovery(); 1563 if (!skipDetectionReport) reportSessionDetection(); 1564 }); 1565 } 1566 1567 detectItpHeuristic(); 1568 1569 // Preview mode: bypass detection entirely and show wall directly on every load. 1570 // Defer until body exists -- the loader may run as a sync <script> in <head>. 1571 if (location.search.indexOf('_qaxal_abwall_preview') !== -1) { 1572 if (document.body) { 1573 applyAdblockWall(); 1574 } else { 1575 document.addEventListener('DOMContentLoaded', applyAdblockWall); 1576 } 1577 } else { 1578 runBlockednessTestAsync(); 1579 } 1580 1581 function isBlockingEnvironment(){ 1582 return getAbState() === "1"; 1583 } 1584 1585 var clientSessionId = getClientSessionId(); // technical _sid â unconditional, never linked to fpid 1586 1587 function getSignalHeaders(){ 1588 var h = {}; 1589 if (clientSessionId) h["x-qaxal-sid"] = clientSessionId; 1590 if (isBlockingEnvironment()) h["x-qaxal-adblock"] = "1"; 1591 if (getItpState()) h["x-qaxal-itp"] = "1"; 1592 return h; 1593 } 1594 1595 1596 // ---- Re-runnable boot ---- 1597 // Pre-consent new visitor boots with _sid only (no cid/fpid); GTM loads in 1598 // consent-denied mode. On consent, runBoot() re-fires with the newly-minted 1599 // fpid so cookie-keeper/attribution records get created. Idempotent. 1600 var __qaxal_booting = false; // in-flight guard 1601 var __qaxal_booted = false; // a boot has completed 1602 var __qaxal_booted_with_fpid = false; // the completed boot carried an fpid 1603 var __qaxal_boot_again = false; // consent changed while boot was in flight 1604 1605 function runBoot(){ 1606 if (__qaxal_booting) { __qaxal_boot_again = true; return; } 1607 var fpid = resolveFpidForBoot(); 1608 // Nothing to do if we already booted and either had fpid or still have none. 1609 if (__qaxal_booted && (__qaxal_booted_with_fpid || !fpid)) return; 1610 __qaxal_booting = true; 1611 1612 var payload = { 1613 client_session_id: clientSessionId, 1614 loader_token: "2lv7956f", 1615 container_token: "44sf49pu", 1616 url: location.href, // still carries utm/gclid on first pageview 1617 referrer: document.referrer || null, 1618 ua: navigator.userAgent 1619 }; 1620 if (fpid) { payload.cid = fpid; payload.fpid = fpid; } // omit entirely pre-consent 1621 1622 fetch("https://assets.forbes.cz/boot", { 1623 method: "POST", 1624 credentials: "include", 1625 headers: (function(){ 1626 var base = { "content-type": "application/json" }; 1627 var sig = getSignalHeaders(); 1628 for (var k in sig) base[k] = sig[k]; 1629 return base; 1630 })(), 1631 body: JSON.stringify(payload) 1632 }).then(function(r){ 1633 if (!r.ok) { return r.json().catch(function(){ return null; }); } 1634 return r.json(); 1635 }).then(function(data){ 1636 __qaxal_booting = false;
1637 __qaxal_booted = true; 1638 if (fpid) __qaxal_booted_with_fpid = true; 1639 else window.__qaxal_boot_had_no_fpid = true; 1640 // Single unified post-boot path (installs interceptors + injects GTM) 1641 afterBoot(data); 1642 if (__qaxal_boot_again) { 1643 __qaxal_boot_again = false; 1644 runBoot(); 1645 } 1646 }).catch(function(e){ 1647 __qaxal_booting = false; 1648 console && console.warn && console.warn("v2 loader boot error", e); 1649 if (__qaxal_boot_again) { 1650 __qaxal_boot_again = false; 1651 runBoot(); 1652 } 1653 }); 1654 } 1655 1656 // Fire initial boot (with or without fpid, depending on consent). 1657 runBoot(); 1658 1659 // ---- Re-consent lifecycle ---- 1660 // Re-run boot when consent becomes present. Covers: banner accept after load, 1661 // consent granted before init (handled by runBoot's first read), SPA / late 1662 // banner, cross-tab accept, and gtag consent update. All idempotent. 1663 var __qaxal_last_consent_state = null; 1664 var __qaxal_consent_watch_installed = false; 1665 function onConsentMaybeChanged(){ 1666 var state = consentState(); 1667 if (state === __qaxal_last_consent_state) return; 1668 __qaxal_last_consent_state = state; 1669 if (state === "granted") { mintFpid(); runBoot(); return; } 1670 if (state === "denied") purgeFpid(); 1671 } 1672 function installConsentWatch(){ 1673 if (__qaxal_consent_watch_installed) return; 1674 __qaxal_consent_watch_installed = true; 1675 1676 // (a) dataLayer 'consent_updated' / gtag('consent','update',...) â primary signal 1677 try { 1678 window.dataLayer = window.dataLayer || []; 1679 var _push = window.dataLayer.push; 1680 window.dataLayer.push = function(){ 1681 try { 1682 for (var i = 0; i < arguments.length; i++){ 1683 var a = arguments[i]; 1684 if (a && (a.event === "consent_updated" || (a[0] === "consent" && a[1] === "update"))) { 1685 setTimeout(onConsentMaybeChanged, 0); // let the cookie write settle 1686 } 1687 } 1688 } catch(e){} 1689 return _push.apply(this, arguments); 1690 }; 1691 } catch(e){} 1692 1693 // (b) storage event â consent set in another tab 1694 try { 1695 window.addEventListener("storage", function(e){ 1696 if (!e || e.key === null || e.key === "qaxal_consent") onConsentMaybeChanged(); 1697 }); 1698 } catch(e){} 1699 1700 // (c) Durable poll â hooks above are latency optimisations only. GTM may 1701 // replace dataLayer.push, and third-party CMPs can publish consent long 1702 // after page load or without touching qaxal_consent. 1703 setInterval(onConsentMaybeChanged, 1000); 1704 onConsentMaybeChanged(); 1705 } 1706 installConsentWatch(); 1707})();
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.