PageSourceSearch

https://new-morning-retreat.vercel.app/dgb-consent.js

js new-morning-retreat.vercel.app collected 2026-10-03 07:22:31 UTC 7,155 bytes, 146 lines download raw bytes

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.