1// ââ GOOGLE CONSENT MODE v2 + TAG MANAGER + HUBSPOT ââââââââââââ 2// Analytics-only, "basic" consent mode: GTM/GA4 is NOT loaded at all until 3// the visitor consents, so no request reaches Google beforehand. Only 4// analytics_storage is ever granted â no advertising/remarketing signals. 5// 6// The HubSpot tracking loader is handled the same way. It used to sit as a 7// static <script id="hs-script-loader"> in every HTML file and therefore 8// connected to js.hs-scripts.com before the visitor had decided anything. 9// It is now injected here, only after consent. 10// 11// Not affected, on purpose: the HubSpot form embed on /contact 12// (js.hsforms.net) stays static â without it the contact form cannot render, 13// and reaching us is the very service the visitor asked for. The meetings 14// embed has its own separate gate further down (seerene_hs_consent). 15(function(){ 16 window.dataLayer = window.dataLayer || []; 17 function gtag(){ dataLayer.push(arguments); } 18 window.gtag = gtag; 19 20 // Deny everything by default (GDPR / § 25 TDDDG). No wait_for_update: the 21 // container is only injected after consent, so there is nothing to wait for. 22 gtag('consent', 'default', { 23 ad_storage: 'denied', 24 ad_user_data: 'denied', 25 ad_personalization: 'denied', 26 analytics_storage: 'denied', 27 functionality_storage: 'granted', 28 security_storage: 'granted' 29 }); 30 31 var gtmLoaded = false; 32 function loadGTM(){ 33 if(gtmLoaded) return; 34 gtmLoaded = true;
vendor: 399 bytes, lines 34-44
34 35 (function(w,d,s,l,i){ 36 w[l]=w[l]||[]; 37 w[l].push({'gtm.start': new Date().getTime(), event:'gtm.js'}); 38 var f=d.getElementsByTagName(s)[0], 39 j=d.createElement(s), 40 dl=l!='dataLayer'?'&l='+l:''; 41 j.async=true; 42 j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl; 43 f.parentNode.insertBefore(j,f); 44 })(window,document,'script','dataLayer','
44GTM-PW83VQ
vendor: 6 bytes, lines 44-45
44'); 45
45} 46 47 var hsLoaded = false; 48 function loadHubSpot(){ 49 if(hsLoaded) return; 50 hsLoaded = true; 51 var s = document.createElement('script'); 52 s.type = 'text/javascript'; 53 s.id = 'hs-script-loader'; 54 s.async = true; 55 s.defer = true; 56 s.src = 'https://js.hs-scripts.com/4856350.js'; 57 document.head.appendChild(s); 58 } 59 60 // Called by the cookie banner on "Accept": grant analytics only, then load. 61 window.seereneGrantAnalytics = function(){ 62 gtag('consent', 'update', { analytics_storage: 'granted' }); 63 loadGTM(); 64 loadHubSpot(); 65 }; 66 67 // Returning visitor who already consented â grant + load immediately. 68 try{ 69 var v = localStorage.getItem('seerene_cookie_consent'); 70 if(v){ var d = JSON.parse(v); if(d && d.expires > Date.now() && d.value === 'accepted') window.seereneGrantAnalytics(); } 71 }catch(e){} 72})(); 73 74// ââ GALLERY LIGHTBOX ââââââââââââââââââââââââââââââââââââââââââ 75(function(){ 76 var items = [], current = 0; 77 function init(){ 78 if(!document.querySelector('.ev-gallery')) return; 79 var lb = document.createElement('div'); 80 lb.className = 'lb'; 81 lb.setAttribute('role','dialog'); 82 lb.setAttribute('aria-modal','true'); 83 lb.setAttribute('aria-label','Image viewer'); 84 lb.setAttribute('tabindex','-1'); 85 lb.innerHTML = 86 '<button class="lb-close" aria-label="Close">\u00d7</button>'+ 87 '<button class="lb-prev" aria-label="Previous image">‹</button>'+ 88 '<img class="lb-img" src="" alt="">'+ 89 '<button class="lb-next" aria-label="Next image">›</button>'+ 90 '<div class="lb-caption"></div>'+ 91 '<div class="lb-counter"></div>'; 92 document.body.appendChild(lb); 93 var lbImg = lb.querySelector('.lb-img'); 94 var lbCap = lb.querySelector('.lb-caption'); 95 var lbCnt = lb.querySelector('.lb-counter'); 96 function show(i){ 97 current = (i + items.length) % items.length; 98 var img = items[current].querySelector('img'); 99 lbImg.src = items[current].href; 100 lbImg.alt = img ? img.alt : ''; 101 lbCap.textContent = img ? img.alt : ''; 102 lbCnt.textContent = (current + 1) + ' / ' + items.length; 103 lb.classList.add('lb-open'); 104 document.body.style.overflow = 'hidden'; 105 lb.focus(); 106 } 107 function close(){ 108 lb.classList.remove('lb-open'); 109 document.body.style.overflow = ''; 110 } 111 document.querySelectorAll('.ev-gallery').forEach(function(g){ 112 var links = Array.from(g.querySelectorAll('a')); 113 links.forEach(function(a, i){ 114 a.addEventListener('click', function(e){ 115 e.preventDefault(); 116 items = links; 117 show(i); 118 }); 119 }); 120 }); 121 lb.querySelector('.lb-close').addEventListener('click', close); 122 lb.querySelector('.lb-prev').addEventListener('click', function(e){ e.stopPropagation(); show(current - 1); }); 123 lb.querySelector('.lb-next').addEventListener('click', function(e){ e.stopPropagation(); show(current + 1); }); 124 lb.addEventListener('click', function(e){ if(e.target === lb) close(); }); 125 // Touch swipe 126 var tx = 0; 127 lb.addEventListener('touchstart', function(e){ tx = e.touches[0].clientX; }, {passive:true}); 128 lb.addEventListener('touchend', function(e){ 129 var dx = e.changedTouches[0].clientX - tx; 130 if(Math.abs(dx) > 50){ dx < 0 ? show(current + 1) : show(current - 1); } 131 }); 132 document.addEventListener('keydown', function(e){ 133 if(!lb.classList.contains('lb-open')) return; 134 if(e.key === 'Escape') close(); 135 if(e.key === 'ArrowLeft') show(current - 1); 136 if(e.key === 'ArrowRight') show(current + 1); 137 }); 138 } 139 document.addEventListener('DOMContentLoaded', init); 140})(); 141 142// ââ NAV: Hide on scroll down, show on scroll up âââââââââââââââ 143(function(){ 144 const nav = document.getElementById('main-nav'); 145 if(!nav) return; 146 let lastY = 0, ticking = false; 147 window.addEventListener('scroll', function(){ 148 if(!ticking){ 149 requestAnimationFrame(function(){ 150 const y = window.scrollY; 151 if(y > 80){ 152 nav.classList.toggle('nav-hidden', y > lastY); 153 } else { 154 nav.classList.remove('nav-hidden'); 155 } 156 lastY = y; 157 ticking = false;
158 }); 159 ticking = true; 160 } 161 }, {passive: true}); 162})(); 163 164// ââ NAV: Active page highlight (multi-page) âââââââââââââââââââ 165(function(){ 166 const path = window.location.pathname; 167 let page = path.split('/').pop() || 'index.html'; 168 // News article pages: parent submenu "News" (news.html) should stay active 169 const inNewsDir = /\/news\//.test(path); 170 document.querySelectorAll('.nm a, .nav-drawer a').forEach(function(a){ 171 const href = a.getAttribute('href'); 172 if(!href) return; 173 if(a.classList.contains('nb') || a.classList.contains('nav-login')) return; 174 const hrefPage = href.split('#')[0].split('/').pop(); 175 if(!hrefPage) return; 176 const match = hrefPage === page 177 || (page === '' && hrefPage === 'index.html') 178 || (inNewsDir && hrefPage === 'news.html'); 179 if(match){ 180 a.classList.add('nav-active'); 181 // If this link sits inside a submenu, mark the parent trigger too 182 const subParent = a.closest('.has-sub'); 183 if(subParent){ 184 const parentTrigger = subParent.querySelector(':scope > a'); 185 if(parentTrigger) parentTrigger.classList.add('nav-active'); 186 } 187 } 188 }); 189})(); 190 191// ââ NAV: About submenu (desktop click-toggle + mobile toggle) â 192(function(){ 193 var desktopItems = []; 194 document.querySelectorAll('.has-sub').forEach(function(item){ 195 var trigger = item.querySelector(':scope > a'); 196 if(!trigger) return; 197 var isMobile = item.closest('.nav-drawer'); 198 199 // Both desktop and mobile: click on parent toggles the submenu 200 // instead of navigating. Submenu links handle navigation. 201 trigger.addEventListener('click', function(e){ 202 e.preventDefault(); 203 var open = item.getAttribute('aria-expanded') === 'true'; 204 item.setAttribute('aria-expanded', open ? 'false' : 'true'); 205 }); 206 207 if(!isMobile){ 208 desktopItems.push(item); 209 // Space toggles for keyboard users (Enter already fires click) 210 trigger.addEventListener('keydown', function(e){ 211 if(e.key === ' '){ 212 e.preventDefault(); 213 var open = item.getAttribute('aria-expanded') === 'true'; 214 item.setAttribute('aria-expanded', open ? 'false' : 'true'); 215 } 216 if(e.key === 'Escape'){ 217 item.setAttribute('aria-expanded','false'); 218 trigger.blur(); 219 } 220 }); 221 // Close when focus leaves the whole subtree (keyboard) 222 item.addEventListener('focusout', function(e){ 223 if(!item.contains(e.relatedTarget)) item.setAttribute('aria-expanded','false'); 224 }); 225 } 226 }); 227 228 // Desktop: click outside any open submenu closes it 229 if(desktopItems.length){ 230 document.addEventListener('click', function(e){ 231 desktopItems.forEach(function(item){ 232 if(!item.contains(e.target)) item.setAttribute('aria-expanded','false'); 233 }); 234 }); 235 } 236})(); 237 238// ââ NAV: Hamburger toggle âââââââââââââââââââââââââââââââââââââ 239(function(){ 240 const burger = document.querySelector('.nav-burger'); 241 const drawer = document.getElementById('nav-drawer'); 242 const nav = document.getElementById('main-nav'); 243 if(!burger || !drawer) return; 244 245 function closeDrawer(){ 246 drawer.classList.remove('open'); 247 burger.setAttribute('aria-expanded','false'); 248 burger.setAttribute('aria-label','Open menu'); 249 document.body.style.overflow = ''; 250 if(nav) nav.classList.remove('nav-hidden'); 251 } 252 253 burger.addEventListener('click', function(){ 254 const isOpen = drawer.classList.contains('open'); 255 if(isOpen){ closeDrawer(); } 256 else { 257 drawer.classList.add('open'); 258 burger.setAttribute('aria-expanded','true'); 259 burger.setAttribute('aria-label','Close menu'); 260 document.body.style.overflow = 'hidden'; 261 if(nav) nav.classList.remove('nav-hidden'); 262 } 263 }); 264 265 drawer.querySelectorAll('a').forEach(function(a){ 266 if(a.getAttribute('aria-haspopup')) return; // submenu toggles don't close drawer 267 a.addEventListener('click', closeDrawer); 268 }); 269 270 document.addEventListener('keydown', function(e){ 271 if(e.key === 'Escape') closeDrawer(); 272 }); 273})(); 274 275// ââ STICKY CTA: padding-bottom only if element exists ââââââââââ 276(function(){ 277 var el = document.getElementById('sticky-cta'); 278 if(!el) return;
279 function update(){ 280 if(window.innerWidth <= 900){ 281 document.body.style.paddingBottom = el.offsetHeight + 'px'; 282 } else { 283 document.body.style.paddingBottom = ''; 284 } 285 } 286 update(); 287 window.addEventListener('resize', update, {passive:true}); 288})(); 289 290// ââ COOKIE CONSENT âââââââââââââââââââââââââââââââââââââââââââââ 291(function(){ 292 const slot = document.getElementById('booking-slot'); 293 if(!slot) return; 294 295 const STORAGE_KEY = 'seerene_hs_consent'; 296 const EXPIRY_DAYS = 30; 297 298 function loadHubSpot(){ 299 slot.innerHTML = '<div class="meetings-iframe-container" data-src="https://meetings.hubspot.com/seerene/30-min-demo?embed=true"></div>'; 300 var s = document.createElement('script'); 301 s.src = 'https://static.hsappstatic.net/MeetingsEmbed/ex/MeetingsEmbedCode.js'; 302 s.type = 'text/javascript'; 303 document.body.appendChild(s); 304 var box = slot.closest('.hs-widget-box'); 305 if(box){ 306 var obs = new MutationObserver(function(){ 307 var iframe = slot.querySelector('iframe'); 308 if(iframe){ 309 obs.disconnect(); 310 iframe.addEventListener('load', function(){ box.style.border = 'none'; }, {once:true}); 311 } 312 }); 313 obs.observe(slot, {childList:true, subtree:true}); 314 } 315 } 316 317 function showFallback(){ 318 slot.innerHTML = '<div id="booking-fallback"><p>This booking form uses cookies from HubSpot.<br>You have declined consent.</p><a href="https://www.seerene.com/book-a-demo" target="_blank" rel="noopener noreferrer">Book directly on seerene.com â</a></div>'; 319 } 320 321 function showConsent(){ 322 slot.innerHTML = '<div id="booking-consent"><p>This booking form is provided by HubSpot and sets cookies when loaded. By clicking "Accept" you consent to loading the external script.</p><p><a href="https://www.seerene.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a> · <a href="mailto:[email protected]">Contact us by email instead</a></p><div class="cb-actions"><button class="cb-decline" id="cb-decline">Decline</button><button class="cb-accept" id="cb-accept">Accept & load form</button></div></div>'; 323 document.getElementById('cb-accept').addEventListener('click', function(){ 324 setConsent('accepted'); loadHubSpot(); 325 }); 326 document.getElementById('cb-decline').addEventListener('click', function(){ 327 setConsent('declined'); showFallback(); 328 }); 329 } 330 331 function getConsent(){ 332 try{ 333 const v = localStorage.getItem(STORAGE_KEY); 334 if(!v) return null; 335 const d = JSON.parse(v); 336 return d.expires > Date.now() ? d.value : null; 337 } catch(e){ return null; } 338 } 339 function setConsent(val){ 340 const expires = Date.now() + EXPIRY_DAYS * 864e5; 341 try{ localStorage.setItem(STORAGE_KEY, JSON.stringify({value:val, expires})); } catch(e){} 342 } 343 344 const existing = getConsent(); 345 if(existing === 'accepted'){ loadHubSpot(); } 346 else if(existing === 'declined'){ showFallback(); } 347 else { showConsent(); } 348})(); 349 350// ââ TO-TOP BUTTON ââââââââââââââââââââââââââââââââââââââââââââââ 351(function(){ 352 const btn = document.getElementById('to-top'); 353 if(!btn) return; 354 window.addEventListener('scroll', function(){ 355 btn.classList.toggle('visible', window.scrollY > 400); 356 }, {passive:true}); 357 btn.addEventListener('click', function(e){ 358 e.preventDefault(); 359 window.scrollTo({top:0, behavior:'smooth'}); 360 }); 361})(); 362 363// ââ LEGAL OVERLAYS (Imprint / Privacy) âââââââââââââââââââââââââââ 364(function(){ 365 function openOverlay(id){ 366 var el = document.getElementById(id); 367 if(!el) return; 368 el.classList.add('open'); 369 el.setAttribute('aria-hidden','false'); 370 document.body.style.overflow = 'hidden'; 371 } 372 function closeAllOverlays(){ 373 document.querySelectorAll('.legal-overlay.open').forEach(function(o){ 374 o.classList.remove('open'); 375 o.setAttribute('aria-hidden','true'); 376 }); 377 document.body.style.overflow = ''; 378 } 379 document.addEventListener('DOMContentLoaded', function(){ 380 var imprintBtn = document.getElementById('open-imprint'); 381 var privacyBtn = document.getElementById('open-privacy'); 382 if(imprintBtn) imprintBtn.addEventListener('click', function(e){ e.preventDefault(); openOverlay('imprint-overlay'); }); 383 if(privacyBtn) privacyBtn.addEventListener('click', function(e){ e.preventDefault(); openOverlay('privacy-overlay'); });
384 document.querySelectorAll('.legal-overlay').forEach(function(o){ 385 o.addEventListener('click', function(e){ if(e.target === o) closeAllOverlays(); }); 386 var closeBtn = o.querySelector('[data-close]'); 387 if(closeBtn) closeBtn.addEventListener('click', closeAllOverlays); 388 }); 389 document.addEventListener('keydown', function(e){ if(e.key === 'Escape') closeAllOverlays(); }); 390 // Autodesk modal (about page) 391 var autodeskBtn = document.getElementById('open-autodesk'); 392 if(autodeskBtn) autodeskBtn.addEventListener('click', function(e){ e.preventDefault(); openOverlay('autodesk-overlay'); }); 393 }); 394})(); 395 396// ââ GLOBAL COOKIE CONSENT BANNER (DSGVO) âââââââââââââââââââââââ 397(function(){ 398 var CONSENT_KEY = 'seerene_cookie_consent'; 399 var EXPIRY_DAYS = 365; 400 401 function getConsent(){ 402 try{ 403 var v = localStorage.getItem(CONSENT_KEY); 404 if(!v) return null; 405 var d = JSON.parse(v); 406 return d.expires > Date.now() ? d.value : null; 407 } catch(e){ return null; } 408 } 409 410 function setConsent(val){ 411 var expires = Date.now() + EXPIRY_DAYS * 864e5; 412 try{ localStorage.setItem(CONSENT_KEY, JSON.stringify({value:val, expires:expires})); } catch(e){} 413 } 414 415 // ââ YouTube wrapper ââââââââââââââââââââââââââââââââââââââââââ 416 function wrapYouTubeIframes(){ 417 document.querySelectorAll('iframe[src*="youtube.com/embed"]').forEach(function(iframe){ 418 var container = iframe.parentNode; 419 var containerHTML = container.outerHTML; 420 421 var wrap = document.createElement('div'); 422 wrap.className = 'yt-consent-wrap'; 423 wrap.dataset.ytContainer = containerHTML; 424 wrap.innerHTML = 425 '<p>This video is provided by YouTube (Google LLC).' + 426 ' Loading it sets cookies that allow your data to be transferred to the USA.</p>' + 427 '<button class="yt-load-btn">Load video</button>' + 428 '<a href="/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>'; 429 wrap.querySelector('.yt-load-btn').addEventListener('click', function(){ 430 setConsent('accepted'); 431 activateAll(); 432 }); 433 container.parentNode.replaceChild(wrap, container); 434 }); 435 } 436 437 function activateAll(){ 438 document.querySelectorAll('.yt-consent-wrap').forEach(function(wrap){ 439 var tmp = document.createElement('div'); 440 tmp.innerHTML = wrap.dataset.ytContainer; 441 wrap.parentNode.replaceChild(tmp.firstElementChild, wrap); 442 }); 443 hideBanner(); 444 } 445 446 // ââ Banner âââââââââââââââââââââââââââââââââââââââââââââââââââ 447 function createBanner(){ 448 var banner = document.createElement('div'); 449 banner.id = 'cookie-banner'; 450 banner.innerHTML = 451 '<p>This website uses cookies and external services (e. g. YouTube). Learn more in our' + 452 ' <a href="/privacy" rel="noopener noreferrer">Privacy Policy</a>.</p>' + 453 '<div class="cb-actions">' + 454 '<button class="cb-decline" id="cb-global-decline">Decline</button>' + 455 '<button class="cb-accept" id="cb-global-accept">Accept</button>' + 456 '</div>'; 457 document.body.appendChild(banner); 458 // Double rAF ensures transition fires after paint 459 requestAnimationFrame(function(){ 460 requestAnimationFrame(function(){ banner.classList.add('cb-show'); }); 461 }); 462 document.getElementById('cb-global-accept').addEventListener('click', function(){ 463 setConsent('accepted'); 464 if(window.seereneGrantAnalytics){ window.seereneGrantAnalytics(); } 465 activateAll(); 466 }); 467 document.getElementById('cb-global-decline').addEventListener('click', function(){ 468 setConsent('declined'); 469 // Stops collection if the visitor previously accepted this session. 470 if(window.gtag){ window.gtag('consent', 'update', { analytics_storage:'denied' }); } 471 hideBanner(); 472 }); 473 } 474 475 function hideBanner(){ 476 var banner = document.getElementById('cookie-banner'); 477 if(banner){ 478 banner.classList.remove('cb-show'); 479 setTimeout(function(){ if(banner.parentNode) banner.parentNode.removeChild(banner); }, 400); 480 } 481 } 482 483 // Re-open the banner so visitors can change or withdraw their choice 484 // (GDPR Art. 7(3)). Called from the "Manage cookie settings" button in the 485 // Privacy Policy. 486 window.seereneReopenConsent = function(){ 487 if(document.getElementById('cookie-banner')) return; 488 createBanner(); 489 }; 490 491 document.addEventListener('DOMContentLoaded', function(){ 492 var consent = getConsent(); 493 if(consent !== 'accepted'){ 494 wrapYouTubeIframes(); 495 } 496 if(consent === null){ 497 createBanner(); 498 } 499 }); 500})();
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.