PageSourceSearch

https://www.seerene.com/scripts.js?v=1c63b4cf

js seerene.com collected 2026-10-03 19:32:06 UTC 19,993 bytes, 500 lines download raw bytes

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">&#8249;</button>'+
88      '<img class="lb-img" src="" alt="">'+
89      '<button class="lb-next" aria-label="Next image">&#8250;</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> &nbsp;·&nbsp; <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 &amp; 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.&thinsp;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.