PageSourceSearch

https://holzfilm.com/consent.js?v=20261007-ui1

js holzfilm.com collected 2026-10-07 23:41:31 UTC 24,109 bytes, 474 lines download raw bytes

1/*
2 * holzfilm Consent Manager
3 * Consent-before-load for GTM and Vimeo, service-level choices and revocation.
4 */
5(function () {
6  "use strict";
7
8  const STORAGE_KEY = "holzfilm_consent_v3";
9  const CONSENT_VERSION = 3;
10  const MAX_AGE_MS = 180 * 24 * 60 * 60 * 1000;
11  const GTM_ID = "GTM-WQW3SHD";
12  const services = ["analytics", "hotjar", "googleAds", "facebook", "linkedIn", "rb2b", "vimeo"];
13  const trackingServices = services.filter((name) => name !== "vimeo");
14  const currentScriptUrl = document.currentScript?.src ? new URL(document.currentScript.src, location.href) : new URL(location.href);
15  const baseUrl = new URL("./", currentScriptUrl);
16  const legalUrl = (page) => new URL(`${page}/index.html`, baseUrl).href;
17
18  window.dataLayer = window.dataLayer || [];
19  window.gtag = window.gtag || function () { window.dataLayer.push(arguments); };
20  window.gtag("consent", "default", {
21    ad_storage: "denied",
22    analytics_storage: "denied",
23    ad_user_data: "denied",
24    ad_personalization: "denied",
25    functionality_storage: "granted",
26    security_storage: "granted",
27    wait_for_update: 500
28  });
29
30  const blankChoices = () => Object.fromEntries(services.map((name) => [name, false]));
31
32  function normalizeChoices(value) {
33    const normalized = blankChoices();
34    services.forEach((name) => { normalized[name] = Boolean(value?.[name]); });
35    return normalized;
36  }
37
38  function readDecision() {
39    try {
40      const parsed = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null");
41      if (!parsed || parsed.version !== CONSENT_VERSION || !parsed.decidedAt) return null;
42      if (Date.now() - new Date(parsed.decidedAt).getTime() > MAX_AGE_MS) return null;
43      return { ...parsed, choices: normalizeChoices(parsed.choices) };
44    } catch (_) {
45      return null;
46    }
47  }
48
49  function writeDecision(choices, source) {
50    const decision = {
51      version: CONSENT_VERSION,
52      decidedAt: new Date().toISOString(),
53      source,
54      choices: normalizeChoices(choices)
55    };
56    try { localStorage.setItem(STORAGE_KEY, JSON.stringify(decision)); } catch (_) {}
57    return decision;
58  }
59
60  let decision = readDecision();
61  let gtmLoaded = false;
62  let layer = null;
63  let lastFocus = null;
64  let requestedService = null;
65
66  function has(service) {
67    return Boolean(decision?.choices?.[service] && Date.now() - new Date(decision.decidedAt).getTime() < MAX_AGE_MS);
68  }
69  function anyTracking(choices) { return trackingServices.some((name) => Boolean(choices[name])); }
70
71  function serviceForResource(value) {
72    if (!value) return null;
73    let url;
74    try { url = new URL(String(value), location.href); } catch (_) { return null; }
75    const host = url.hostname.toLowerCase();
76    const path = url.pathname.toLowerCase();
77
78    if (host === "www.googletagmanager.com" && path.endsWith("/gtm.js")) return null;
79    if (host === "www.googletagmanager.com" && path.endsWith("/gtag/js")) {
80      const id = (url.searchParams.get("id") || "").toUpperCase();
81      if (id.startsWith("AW-")) return "googleAds";
82      if (id.startsWith("G-") || id.startsWith("UA-")) return "analytics";
83    }
84    if (/googleadservices\.com$|doubleclick\.net$/.test(host)) return "googleAds";
85    if (/hotjar\.com$|hotjar\.io$/.test(host)) return "hotjar";
86    if (/facebook\.net$|facebook\.com$/.test(host)) return "facebook";
87    if (/linkedin\.com$|licdn\.com$/.test(host)) return "linkedIn";
88    if (/rb2b\.com$|retention\.com$/.test(host) || host === "ddwl4m2hdecbv.cloudfront.net") return "rb2b";
89    return null;
90  }
91
92  function shouldBlockNode(node) {
93    if (!node || node.nodeType !== 1 || !/^(SCRIPT|IMG|IFRAME)$/.test(node.tagName)) return false;
94    const service = serviceForResource(node.src || node.getAttribute?.("src"));
95    return Boolean(service && !has(service));
96  }
97
98  (function installResourceGuard() {
99    const nativeAppendChild = Node.prototype.appendChild;
100    const nativeInsertBefore = Node.prototype.insertBefore;
101    const nativeAppend = Element.prototype.append;
102    const nativePrepend = Element.prototype.prepend;
103
104    Node.prototype.appendChild = function (node) {
105      if (shouldBlockNode(node)) return node;
106      return nativeAppendChild.call(this, node);
107    };
108    Node.prototype.insertBefore = function (node, reference) {
109      if (shouldBlockNode(node)) return node;
110      return nativeInsertBefore.call(this, node, reference);
111    };
112    Element.prototype.append = function (...nodes) {
113      return nativeAppend.apply(this, nodes.filter((node) => !shouldBlockNode(node)));
114    };
115    Element.prototype.prepend = function (...nodes) {
116      return nativePrepend.apply(this, nodes.filter((node) => !shouldBlockNode(node)));
117    };
118  })();
119
120  function pushLegacyEvents(choices) {
121    const events = {
122      analytics: ["AnalyticsOptIn", "AnalyticsOptOut"],
123      hotjar: ["HotjarOptIn", "HotjarOptOut"],
124      googleAds: ["GoogleAdsOptIn", "GoogleAdsOptOut"],
125      facebook: ["PixelOptIn", "PixelOptOut"],
126      linkedIn: ["LinkedInOptIn", "LinkedInOptOut"],
127      rb2b: ["RB2BOptIn", "RB2BOptOut"],
128      vimeo: ["VimeoOptIn", "VimeoOptOut"]
129    };
130    Object.entries(events).forEach(([name, names]) => window.dataLayer.push({ event: choices[name] ? names[0] : names[1] }));
131  }
132
133  function updateGoogleConsent(choices) {
134    window.gtag("consent", "update", {
135      analytics_storage: choices.analytics ? "granted" : "denied",
136      ad_storage: choices.googleAds ? "granted" : "denied",
137      ad_user_data: choices.googleAds ? "granted" : "denied",
138      // This campaign measures leads; it does not build remarketing audiences.
139      ad_personalization: "denied"
140    });
141  }
142
143  function loadGtm() {
144    if (gtmLoaded || document.querySelector(`script[data-hf-gtm="${GTM_ID}"]`)) return;
145    gtmLoaded = true;
146    window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
147    const script = document.createElement("script");
148    script.async = true;
149    script.dataset.hfGtm = GTM_ID;
150    script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(GTM_ID)}`;
151    document.head.appendChild(script);
152  }
153
154  function applyDecision(nextDecision) {
155    decision = nextDecision;
156    updateGoogleConsent(decision.choices);
157    pushLegacyEvents(decision.choices);
158    if (anyTracking(decision.choices)) loadGtm();
159    document.dispatchEvent(new CustomEvent("holzfilm:consentchange", { detail: { ...decision } }));
160  }
161
162  function expireCookie(name, domain, path = "/") {
163    const domainPart = domain ? `; domain=${domain}` : "";
164    document.cookie = `${name}=; Max-Age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}${domainPart}; SameSite=Lax`;
165  }
166
167  function clearKnownCookies(next) {
168    const denied = (name) => !next?.[name];
169    if (denied("googleAds")) {
170      try {
171        localStorage.removeItem("holzfilm_ads_attribution_v1");
172        localStorage.removeItem("_gcl_ls");
173      } catch (_) {}
174    }
175    // A withdrawal must not erase another, still permitted service's cookies.
176    const cookiesByService = {
177      analytics: [/^_ga/, /^_gid$/, /^_gat/],
178      googleAds: [/^_gcl/],
179      facebook: [/^_fbp$/, /^_fbc$/, /^fb-pixel-is-disabled$/],
180      hotjar: [/^_hj/],
181      linkedIn: [/^li_/, /^bcookie$/, /^bscookie$/, /^lidc$/, /^UserMatchHistory$/, /^AnalyticsSyncHistory$/],
182      rb2b: [/^_reb2/i, /^_rb2b/i]
183    };
184    const known = Object.entries(cookiesByService)
185      .filter(([service]) => denied(service))
186      .flatMap(([, patterns]) => patterns);
187    const host = location.hostname;
188    const root = host.split(".").slice(-2).join(".");
189    document.cookie.split(";").forEach((part) => {
190      const name = part.split("=")[0].trim();
191      if (!known.some((pattern) => pattern.test(name))) return;
192      const paths = new Set(["/"]);
193      const segments = location.pathname.split("/").filter(Boolean);
194      segments.forEach((_, index) => {
195        const path = `/${segments.slice(0, index + 1).join("/")}`;
196        paths.add(path);
197        paths.add(`${path}/`);
198      });
199      new Set(["", host, `.${host}`, root, `.${root}`]).forEach((domain) => {
200        paths.forEach((path) => expireCookie(name, domain, path));
201      });
202    });
203    if (denied("vimeo")) {
204      try {
205        Object.keys(sessionStorage).filter((key) => key.startsWith("holzfilm:vimeo-thumbnail:")).forEach((key) => sessionStorage.removeItem(key));
206      } catch (_) {}
207    }
208  }
209
210  function serviceMarkup(name, title, copy, provider, privacyUrl) {
211    return `<article class="hf-consent-service">
212      <div><h4>${title}</h4><p>${copy}<br>Anbieter: ${provider} · <a href="${privacyUrl}" target="_blank" rel="noopener noreferrer">Datenschutz</a></p></div>
213      <label class="hf-consent-switch"><input type="checkbox" data-hf-service="${name}" aria-label="${title} erlauben"><span aria-hidden="true"></span></label>
214    </article>`;
215  }
216
217  const chevronMarkup = `<span class="hf-consent-chevron" aria-hidden="true"><svg viewBox="0 0 20 20"><path d="m6 8 4 4 4-4"/></svg></span>`;
218
219  function renderLayer() {
220    if (layer) return layer;
221    layer = document.createElement("dialog");
222    layer.className = "hf-consent-layer";
223    layer.hidden = true;
224    layer.setAttribute("aria-labelledby", "hf-consent-title");
225    layer.innerHTML = `<section class="hf-consent-panel">
226      <div class="hf-consent-view" data-hf-view="intro">
227        <div class="hf-consent-intro">
228          <div>
229            <p class="hf-consent-kicker">Datenschutz</p>
230            <h2 class="hf-consent-title" id="hf-consent-title">Cookies und externe Dienste</h2>
231            <p class="hf-consent-copy">Optionale Analyse-, Marketing- und Vimeo-Dienste laden wir nur mit Ihrer Einwilligung. Ohne Auswahl bleiben sie deaktiviert; die Website ist weiterhin nutzbar.</p>
232          </div>
233          <div class="hf-consent-actions">
234            <button class="hf-consent-button hf-consent-button--primary" type="button" data-hf-action="accept">Alle akzeptieren</button>
235            <button class="hf-consent-button" type="button" data-hf-action="reject">Alle ablehnen</button>
236            <button class="hf-consent-button hf-consent-button--quiet" type="button" data-hf-action="details">Individuell einstellen</button>
237          </div>
238          <nav class="hf-consent-links" aria-label="Rechtliche Informationen"><a href="${legalUrl("impressum")}">Impressum</a><a href="${legalUrl("datenschutz")}">Datenschutz</a></nav>
239        </div>
240      </div>
241      <div class="hf-consent-view hf-consent-view--details" data-hf-view="details" hidden>
242        <header class="hf-consent-head">
243          <div><p class="hf-consent-kicker">Datenschutzeinstellungen</p><h2 id="hf-consent-settings-title">Sie entscheiden.</h2><p>Alle optionalen Dienste sind standardmäßig aus. Ihre Auswahl gilt sechs Monate und kann jederzeit über „Cookie-Einstellungen“ im Footer geändert werden.</p></div>
244          <button class="hf-consent-button hf-consent-back" type="button" data-hf-action="back">Zurück</button>
245        </header>
246        <div class="hf-consent-categories">
247          <details class="hf-consent-category">
248            <summary>${chevronMarkup}<div><h3>Notwendig</h3><p>Speichert nur Ihre Consent-Auswahl lokal im Browser.</p></div><label class="hf-consent-switch"><input type="checkbox" checked disabled aria-label="Notwendige Speicherung aktiv"><span aria-hidden="true"></span></label></summary>
249            <div class="hf-consent-services"><article class="hf-consent-service"><div><h4>holzfilm Consent</h4><p>Zweck: Speichern und Nachweisen Ihrer Auswahl · Speicherort: dieses Gerät · Laufzeit: 6 Monate</p></div></article></div>
250          </details>
251          <details class="hf-consent-category" data-hf-category="media">
252            <summary>${chevronMarkup}<div><h3>Externe Medien</h3><p>Vimeo-Player und Vorschaubilder unserer Filmproduktionen.</p></div><label class="hf-consent-switch"><input type="checkbox" data-hf-master="vimeo" aria-label="Externe Medien erlauben"><span aria-hidden="true"></span></label></summary>
253            <div class="hf-consent-services">${serviceMarkup("vimeo", "Vimeo", "Stellt Video-Player, Vorschaubilder und Wiedergabe bereit. Beim Laden werden insbesondere IP-Adresse sowie Geräte-, Player- und Nutzungsdaten in die USA übermittelt. Wir verwenden den Vimeo-Player mit DNT=1; dennoch können technisch erforderliche Sicherheits-Cookies gesetzt und bereits vorhandene Vimeo-Cookies übermittelt werden. Die Freigabe gilt sechs Monate und ist jederzeit widerrufbar.", "Vimeo.com, Inc. (USA)", "https://vimeo.com/legal/de/datenschutz/policy/")}</div>
254          </details>
255          <details class="hf-consent-category" data-hf-category="statistics">
256            <summary>${chevronMarkup}<div><h3>Statistik</h3><p>Nutzungsanalyse und Optimierung der Website.</p></div><label class="hf-consent-switch"><input type="checkbox" data-hf-master="analytics,hotjar" aria-label="Statistikdienste erlauben"><span aria-hidden="true"></span></label></summary>
257            <div class="hf-consent-services">
258              ${serviceMarkup("analytics", "Google Analytics", "Analyse von Seitenaufrufen, Interaktionen, Geräten und Kampagnen. Typische Cookies: _ga, _gid; Laufzeit je nach GTM-Konfiguration.", "Google Ireland Limited", "https://policies.google.com/privacy?hl=de")}
259              ${serviceMarkup("hotjar", "Hotjar", "Heatmaps, Session-Analyse und Feedback zur Verbesserung der Bedienbarkeit. Typische Cookies beginnen mit _hj.", "Hotjar Ltd.", "https://www.hotjar.com/legal/policies/privacy/")}
260            </div>
261          </details>
262          <details class="hf-consent-category" data-hf-category="marketing">
263            <summary>${chevronMarkup}<div><h3>Marketing</h3><p>Conversion-Messung, Kampagnenoptimierung und B2B-Besucheridentifikation.</p></div><label class="hf-consent-switch"><input type="checkbox" data-hf-master="googleAds,facebook,linkedIn,rb2b" aria-label="Marketingdienste erlauben"><span aria-hidden="true"></span></label></summary>
264            <div class="hf-consent-services">
265              ${serviceMarkup("googleAds", "Google Ads Conversion Tracking", "Ordnet Anfragen Anzeigenkontakten zu. Nach Ihrer Einwilligung speichern wir erste und letzte Anzeigenkontakte mit Zeitpunkt, Einstiegsseite, Kampagnenparametern und Klickkennung höchstens 90 Tage im Browser (holzfilm_ads_attribution_v1) und übermitteln sie mit Ihrer Anfrage an holzfilm. An Google senden wir ein Conversion-Ereignis mit zufälliger Anfrage-ID, keine Formularinhalte. Typische Cookies: _gcl_aw, _gcl_au.", "Google Ireland Limited", "https://policies.google.com/privacy?hl=de")}
266              ${serviceMarkup("facebook", "Meta Pixel", "Misst Conversions und bildet Zielgruppen für Werbung auf Facebook und Instagram. Typisches Cookie: _fbp.", "Meta Platforms Ireland Limited", "https://www.facebook.com/privacy/policy/")}
267              ${serviceMarkup("linkedIn", "LinkedIn Insight Tag", "B2B-Kampagnenmessung und Zielgruppenbildung auf LinkedIn. Typische Cookies: bcookie, lidc, UserMatchHistory.", "LinkedIn Ireland Unlimited Company", "https://www.linkedin.com/legal/privacy-policy")}
268              ${serviceMarkup("rb2b", "RB2B / Retention.com", "Identifiziert mögliche Unternehmensbesucher und kann Websiteaktivitäten mit geschäftlichen Kontaktdaten verknüpfen.", "GetEmails, LLC (USA)", "https://www.rb2b.com/privacy-policy")}
269            </div>
270          </details>
271        </div>
272        <nav class="hf-consent-links" aria-label="Rechtliche Informationen"><a href="${legalUrl("impressum")}">Impressum</a><a href="${legalUrl("datenschutz")}">Datenschutz</a></nav>
273        <div class="hf-consent-detail-actions">
274          <button class="hf-consent-button hf-consent-button--primary" type="button" data-hf-action="save">Auswahl speichern</button>
275          <button class="hf-consent-button" type="button" data-hf-action="reject">Alle ablehnen</button>
276          <button class="hf-consent-button" type="button" data-hf-action="accept">Alle akzeptieren</button>
277        </div>
278      </div>
279    </section>`;
280    document.body.appendChild(layer);
281    bindLayerEvents();
282    return layer;
283  }
284
285  function syncForm(choices = decision?.choices || blankChoices()) {
286    services.forEach((name) => {
287      const input = layer?.querySelector(`[data-hf-service="${name}"]`);
288      if (input) input.checked = Boolean(choices[name]);
289    });
290    syncMasters();
291  }
292
293  function syncMasters() {
294    layer?.querySelectorAll("[data-hf-master]").forEach((master) => {
295      const names = master.dataset.hfMaster.split(",");
296      const values = names.map((name) => Boolean(layer.querySelector(`[data-hf-service="${name}"]`)?.checked));
297      master.checked = values.every(Boolean);
298      master.indeterminate = values.some(Boolean) && !values.every(Boolean);
299    });
300  }
301
302  function showView(name) {
303    layer.querySelectorAll("[data-hf-view]").forEach((view) => { view.hidden = view.dataset.hfView !== name; });
304    layer.setAttribute("aria-labelledby", name === "details" ? "hf-consent-settings-title" : "hf-consent-title");
305    if (name === "details") {
306      syncForm();
307      if (requestedService === "vimeo") layer.querySelector('[data-hf-category="media"]')?.setAttribute("open", "");
308    }
309  }
310
311  function open(options = {}) {
312    renderLayer();
313    requestedService = options.service || null;
314    if (!layer.contains(document.activeElement)) lastFocus = document.activeElement;
315    const details = options.details === true || (Boolean(decision) && options.details !== false);
316    showView(details ? "details" : "intro");
317    if (layer.open) layer.close();
318    layer.hidden = false;
319    layer.classList.toggle("hf-consent-layer--passive", !details);
320    layer.classList.toggle("hf-consent-layer--modal", details);
321    if (details) {
322      // Apply page styles before the native dialog performs layout and focus.
323      document.body.classList.add("modal-open");
324      layer.showModal();
325      window.setTimeout(() => layer.querySelector('[data-hf-view="details"] button')?.focus(), 0);
326    } else {
327      if (!document.querySelector("dialog[open]:not(.hf-consent-layer)")) document.body.classList.remove("modal-open");
328      layer.show();
329    }
330  }
331
332  function close() {
333    if (!layer) return;
334    if (layer.open) layer.close();
335    layer.hidden = true;
336    if (!document.querySelector("dialog[open]")) document.body.classList.remove("modal-open");
337    lastFocus?.focus?.();
338    requestedService = null;
339  }
340
341  function commit(choices, source) {
342    const previous = decision?.choices || blankChoices();
343    const next = normalizeChoices(choices);
344    clearKnownCookies(next);
345    applyDecision(writeDecision(next, source));
346    close();
347    const requiresCleanReload = services.some((name) => previous[name] && !next[name]);
348    if (requiresCleanReload) window.setTimeout(() => location.reload(), 40);
349  }
350
351  function bindLayerEvents() {
352    layer.addEventListener("cancel", (event) => event.preventDefault());
353    layer.querySelectorAll("summary .hf-consent-switch").forEach((control) => {
354      control.addEventListener("click", (event) => event.stopPropagation());
355    });
356    layer.addEventListener("change", (event) => {
357      const master = event.target.closest("[data-hf-master]");
358      if (master) {
359        master.dataset.hfMaster.split(",").forEach((name) => {
360          const input = layer.querySelector(`[data-hf-service="${name}"]`);
361          if (input) input.checked = master.checked;
362        });
363      }
364      syncMasters();
365    });
366    layer.addEventListener("click", (event) => {
367      const action = event.target.closest("[data-hf-action]")?.dataset.hfAction;
368      if (!action) return;
369      if (action === "details") return open({ details: true });
370      if (action === "back") return decision ? close() : open({ details: false });
371      if (action === "accept") return commit(Object.fromEntries(services.map((name) => [name, true])), "accept-all");
372      if (action === "reject") return commit(blankChoices(), "reject-all");
373      if (action === "save") {
374        const choices = blankChoices();
375        services.forEach((name) => { choices[name] = Boolean(layer.querySelector(`[data-hf-service="${name}"]`)?.checked); });
376        return commit(choices, "custom");
377      }
378    });
379    layer.addEventListener("keydown", (event) => {
380      if (event.key !== "Tab" || !layer.classList.contains("hf-consent-layer
380--modal")) return;
381      const focusable = [...layer.querySelectorAll('button:not([disabled]), a[href], input:not([disabled])')].filter((element) => !element.closest("[hidden]"));
382      if (!focusable.length) return;
383      const first = focusable[0];
384      const last = focusable[focusable.length - 1];
385      if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
386      else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
387    });
388  }
389
390  function addFooterControl() {
391    if (document.querySelector("[data-hf-open-consent]")) return;
392    const target = document.querySelector(".footer-bottom > div") || document.querySelector("footer");
393    if (!target) return;
394    const button = document.createElement("button");
395    button.type = "button";
396    button.className = "hf-consent-reopen";
397    button.dataset.hfOpenConsent = "";
398    button.textContent = "Cookie-Einstellungen";
399    button.addEventListener("click", () => open({ details: true }));
400    target.appendChild(button);
401  }
402
403  window.HolzfilmConsent = {
404    has,
405    open,
406    get: () => decision ? JSON.parse(JSON.stringify(decision)) : null,
407    allow(service) {
408      if (!services.includes(service)) return false;
409      if (has(service)) return true;
410      const choices = normalizeChoices(decision?.choices || blankChoices());
411      choices[service] = true;
412      commit(choices, `allow-${service}`);
413      return true;
414    },
415    request(service) {
416      if (has(service)) return Promise.resolve(true);
417      open({ details: true, service });
418      return new Promise((resolve) => {
419        const handler = (event) => {
420          document.removeEventListener("holzfilm:consentchange", handler);
421          resolve(Boolean(event.detail?.choices?.[service]));
422        };
423        document.addEventListener("holzfilm:consentchange", handler);
424      });
425    }
426  };
427
428  window.pushGoogleEvent = function (eventName, eventLabel, properties = {}) {
429    if (!has("analytics")) return false;
430    window.dataLayer.push({
431      event: String(eventName || "click_btn"),
432      event_label: String(eventLabel || "unknown"),
433      ...properties
434    });
435    return true;
436  };
437
438  if (decision) {
439    clearKnownCookies(decision.choices);
440    applyDecision(decision);
441  }
442
443  // Apply withdrawals made in another tab before any later form submission.
444  window.addEventListener("storage", (event) => {
445    if (event.key !== STORAGE_KEY && event.key !== null) return;
446    const previous = decision?.choices || blankChoices();
447    const nextDecision = readDecision();
448    const next = nextDecision?.choices || blankChoices();
449    clearKnownCookies(next);
450    decision = nextDecision;
451    updateGoogleConsent(next);
452    pushLegacyEvents(next);
453    document.dispatchEvent(new CustomEvent("holzfilm:consentchange", { detail: { choices: next } }));
454    if (services.some((name) => previous[name] && !next[name])) location.reload();
455    else if (anyTracking(next)) loadGtm();
456  });
457
458  document.addEventListener("DOMContentLoaded", () => {
459    addFooterControl();
460    document.addEventListener("click", (event) => {
461      const cta = event.target.closest("a.glass-button");
462      if (!cta) return;
463      const segments = location.pathname.split("/").filter(Boolean);
464      const lastSegment = segments[segments.length - 1];
465      const page = (lastSegment === "index.html" ? segments[segments.length - 2] : lastSegment) || "startseite";
466      const label = cta.textContent.trim().toLowerCase().replace(/[^a-z0-9äöüß]+/gi, "_").replace(/^_|_$/g, "").slice(0, 64);
467      window.pushGoogleEvent("click_btn", `cta_${page}_${label}`, {
468        link_url: cta.href,
469        page_path: location.pathname
470      });
471    });
472    if (!decision) open();
473  });
474})();

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.