1/* DGB consent gate v1 (2026-09-30). Source of truth: ~/tools/dgb-consent/dgb-consent.js 2 Every analytics or ad tag on a DGB site loads through this file, or it does not load. 3 4 Why: loading a tracker before the visitor agrees is the fact pattern behind California 5 Invasion of Privacy Act suits ($5,000 per violation) and, next to video, the Video Privacy 6 Protection Act ($2,500 per person). Vault: reference/DGB Legal Risk Standard.md, item L6. 7 8 Use: 9 <script src="/js/dgb-consent.js" defer></script> 10 <script src="/js/analytics.js" defer></script> (after it) 11 in analytics.js: if (GA_ID) dgbConsent.onGrant("analytics", loadGA); 12 in any footer: <a href="#" data-dgb-consent-open>Cookie settings</a> 13 14 Behavior: 15 - Nothing runs until the visitor chooses. No choice, no tracker (fail closed). 16 - The card only appears when a page has actually registered a tracker, so a site 17 whose loader is dormant (no ID pasted) shows nothing and loads nothing. 18 - Global Privacy Control (navigator.globalPrivacyControl) is an automatic "no" for 19 every category, with no card shown. Honoring it is required in California and 20 Colorado and costs us nothing. 21 - Accept and Decline are the same size, weight and color: CPRA regulations treat an 22 asymmetric choice as a dark pattern that voids the consent. 23 - Styled through a constructable stylesheet inside a shadow root, which host CSP 24 style-src rules do not govern, so it renders on strict-CSP sites. 25 - Themeable from the host page: custom properties inherit into the shadow root, so a 26 site sets --dgb-consent-bg / -fg / -border / -btn-bg / -btn-fg on :root. Keep Accept 27 and Decline sharing one button color; that symmetry is the legal point. 28 - Choice kept in localStorage (a category flag, no personal data), for 180 days. 29*/ 30(function () { 31 "use strict"; 32 if (window.dgbConsent) return; 33 34 var KEY = "dgb-consent", VERSION = 1, TTL = 180 * 864e5; 35 var CATS = { analytics: "Analytics", ads: "Advertising" }; 36 var waiting = { analytics: [], ads: [] }; 37 var state = read(); 38 var gpc = navigator.globalPrivacyControl === true; 39 var host = null, shown = false; 40 41 function read() { 42 try { 43 var s = JSON.parse(localStorage.getItem(KEY) || "null"); 44 if (s && s.v === VERSION && Date.now() - s.ts < TTL) return s; 45 } catch (e) {} 46 return null; 47 } 48 function write(analytics, ads) { 49 state = { v: VERSION, ts: Date.now(), analytics: !!analytics, ads: !!ads }; 50 try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (e) {} 51 flush(); 52 } 53 function granted(cat) { 54 if (gpc) return false; 55 return !!(state && state[cat]); 56 } 57 function flush() { 58 Object.keys(waiting).forEach(function (cat) { 59 if (!granted(cat)) return; 60 var q = waiting[cat]; waiting[cat] = []; 61 q.forEach(function (fn) { try { fn(); } catch (e) {} }); 62 }); 63 } 64 function onGrant(cat, fn) { 65 if (!CATS[cat] || typeof fn !== "function") return; 66 if (granted(cat)) { try { fn(); } catch (e) {} return; } 67 waiting[cat].push(fn); 68 if (!state && !gpc) ready(show); 69 } 70 function ready(fn) { 71 if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn, { once: true }); 72 else fn(); 73 } 74 75 var CSS = 76 ":host{all:initial}" + 77 ".c{position:fixed;left:16px;bottom:var(--dgb-consent-bottom,16px);z-index:2147483000;max-width:360px;" + 78 "box-sizing:border-box;padding:16px 18px;border-radius:14px;background:var(--dgb-consent-bg,#15181b);color:var(--dgb-consent-fg,#f2efe8);" + 79 "font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;" + 80 "box-shadow:0 12px 32px rgba(0,0,0,.28);border:1px solid var(--dgb-consent-border,rgba(255,255,255,.12))}" + 81 ".c p{margin:0 0 12px}.c a{color:inherit;text-decoration:underline;text-underline-offset:2px}" + 82 ".row{display:flex;gap:8px}" + 83 "button{flex:1;min-height:44px;padding:0 14px;border-radius:10px;font:600 14px/1 inherit;font-family:inherit;cursor:pointer;" + 84 "background:var(--dgb-consent-btn-bg,#f2efe8);color:var(--dgb-consent-btn-fg,#15181b);border:1px solid var(--dgb-consent-btn-bg,#f2efe8)}" + 85 "button:focus-visible,a:focus-visible{outline:3px solid #8ab4ff;outline-offset:2px}" + 86 "@media (max-width:420px){.c{left:12px;right:12px;max-width:none}}" + 87 "@media (prefers-reduced-motion:no-preference){.c{animation:in .2s ease-out}@keyframes in{from{opacity:0;transform:translateY(8px)}}}"; 88 89 function el(tag, attrs, text) { 90 var n = document.createElement(tag);
91 Object.keys(attrs || {}).forEach(function (k) { n.setAttribute(k, attrs[k]); }); 92 if (text) n.textContent = text; 93 return n; 94 } 95 96 function show(force) { 97 if (shown || gpc) return; 98 var cats = Object.keys(waiting).filter(function (c) { return waiting[c].length; }); 99 if (!cats.length && force !== true) return; 100 shown = true; 101 host = el("div", { "data-dgb-consent": "" }); 102 var root = host.attachShadow({ mode: "open" }); 103 try { 104 var sheet = new CSSStyleSheet(); sheet.replaceSync(CSS); root.adoptedStyleSheets = [sheet]; 105 } catch (e) { 106 root.appendChild(el("style", {}, CSS)); 107 } 108 var names = (cats.length ? cats : Object.keys(CATS)).map(function (c) { return CATS[c].toLowerCase(); }).join(" and "); 109 var box = el("section", { "class": "c", role: "region", "aria-label": "Cookie choices" }); 110 var p = el("p", {}, "May we use " + names + " cookies to see how the site is used? Nothing loads unless you say yes. "); 111 var privacy = document.querySelector('a[href*="privacy"]'); 112 if (privacy) p.appendChild(el("a", { href: privacy.getAttribute("href") }, "Privacy")); 113 var row = el("div", { "class": "row" }); 114 var no = el("button", { type: "button" }, "Decline"); 115 var yes = el("button", { type: "button" }, "Accept"); 116 row.appendChild(no); row.appendChild(yes); 117 box.appendChild(p); box.appendChild(row); 118 root.appendChild(box); 119 no.addEventListener("click", function () { write(false, false); hide(); }); 120 yes.addEventListener("click", function () { write(true, cats.indexOf("ads") > -1); hide(); }); 121 // sit above the pre-sale concept bar when one is showing 122 var bar = document.getElementById("dgb-concept-bar"); 123 if (bar && bar.offsetHeight) box.style.setProperty("--dgb-consent-bottom", bar.offsetHeight + 16 + "px"); 124 document.body.appendChild(host); 125 } 126 function hide() { if (host) { host.remove(); host = null; } shown = false; } 127 128 function open() { 129 hide(); 130 state = null; 131 try { localStorage.removeItem(KEY); } catch (e) {} 132 if (gpc) { alert("Your browser sends Global Privacy Control, so this site loads no analytics or advertising cookies."); return; } 133 show(true); 134 } 135 136 ready(function () { 137 document.addEventListener("click", function (e) { 138 var a = e.target.closest && e.target.closest("[data-dgb-consent-open]"); 139 if (!a) return; 140 e.preventDefault(); 141 open(); 142 }); 143 }); 144 145 window.dgbConsent = { onGrant: onGrant, granted: granted, open: open, gpc: gpc, version: VERSION }; 146})();
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.