PageSourceSearch

https://aumentodelabiosmalaga.com/wp-content/uploads/cookie-consent/cookies.js?v=1.4

js aumentodelabiosmalaga.com collected 2026-09-29 11:41:15 UTC 27,346 bytes, 540 lines download raw bytes

1/*!
2 * PURA Cookie Consent Widget — standalone, self-hosted, zero-dependency.
3 * RGPD / LSSI / AEPD 2024 compliant. Google Consent Mode v2.
4 *
5 * SKELETON / STARTER. Built from the validated mockup (mockup-cookie-consent.html)
6 * and the PRD (cookie-consent-widget-PRD.md). Read IMPLEMENTATION-GUIDE.md before shipping.
7 *
8 * Embed on each money site, in <head>, BEFORE any tracking script:
9 *   <script src="https://<host>/cookies.js"
10 *     data-privacy-url="/politica-de-cookies/"
11 *     data-accent-color="#8B6F4E"></script>
12 *
13 * Tag every tracking <script> so it stays blocked until consent:
14 *
vendor: 123 bytes, lines 14-15
14   <script type="text/plain" data-cookie-category="analytics"
15 *     data-src="https://www.googletagmanager.com/gtag/js?id=
15G-XXXX
vendor: 13 bytes, lines 15-16
15"></script>
16 
16*
17 * Per-site override (optional): define window.ocCookieConfig BEFORE this script
18 * to replace categories / cookie lists / copy. See DEFAULTS below for the shape.
19 */
20(function () {
21  'use strict';
22
23  if (window.__ocCookieWidgetLoaded) { return; }
24  window.__ocCookieWidgetLoaded = true;
25
26  /* ------------------------------------------------------------------ *
27   * 0. Config — read <script> data-* attributes + window.ocCookieConfig
28   * ------------------------------------------------------------------ */
29  var SCRIPT = document.currentScript || (function () {
30    var s = document.getElementsByTagName('script');
31    return s[s.length - 1];
32  })();
33
34  function attr(name, fallback) {
35    var v = SCRIPT && SCRIPT.getAttribute(name);
36    return (v === null || v === undefined || v === '') ? fallback : v;
37  }
38
39  var override = window.ocCookieConfig || {};
40
41  var CONFIG = {
42    privacyUrl:   override.privacyUrl   || attr('data-privacy-url', '/politica-de-cookies/'),
43    lang:         override.lang         || attr('data-lang', 'es'),
44    position:     override.position     || attr('data-position', 'bottom'),      // bottom | bottom-left
45    theme:        override.theme        || attr('data-theme', 'light'),          // light | dark (light only in skeleton)
46    accent:       override.accent       || attr('data-accent-color', '#8B6F4E'),
47    gtmId:        override.gtmId        || attr('data-gtm-id', null),
48    logEndpoint:  override.logEndpoint  || attr('data-log-endpoint', null),
49    expiryDays:   parseInt(override.expiryDays || attr('data-expiry-days', '180'), 10),
50    cookieName:   'oc_cookie_consent',
51    storageKey:   'oc_cookie_consent_detail',
52    version:      '1.0'
53  };
54
55  /* ------------------------------------------------------------------ *
56   * 1. Categories — defaults mirror the PRD. Override per site via
57   *    window.ocCookieConfig.categories. `locked` = always on (necessary).
58   * ------------------------------------------------------------------ */
59  var DEFAULTS = {
60    es: {
61      bannerTitle: 'Utilizamos cookies',
62      bannerText: 'Usamos cookies propias y de terceros para analizar el tráfico y personalizar contenido. Puedes aceptar todas, rechazarlas o configurar tus preferencias.',
63      policyLink: 'Política de cookies',
64      reject: 'Rechazar', accept: 'Aceptar', config: 'Configurar', save: 'Guardar selección',
65      prefsTitle: 'Preferencias de cookies',
66      prefsSubtitle: 'Elige qué cookies permites. Puedes cambiar tus preferencias en cualquier momento.',
67      notice: 'Tus preferencias se guardan durante {DAYS} días. Pasado ese tiempo, te volveremos a preguntar. Puedes cambiarlas en cualquier momento pulsando el icono 🍪 en la esquina inferior izquierda.',
68      cookiesLabel: 'cookies', cookieLabel: 'cookie', close: 'Cerrar', reopenTitle: 'Configurar cookies'
69    },
70    en: {
71      bannerTitle: 'We use cookies',
72      bannerText: 'We use our own and third-party cookies to analyse traffic and personalise content. You can accept all, reject them or configure your preferences.',
73      policyLink: 'Cookie policy',
74      reject: 'Reject', accept: 'Accept', config: 'Configure', save: 'Save selection',
75      prefsTitle: 'Cookie preferences',
76      prefsSubtitle: 'Choose which cookies you allow. You can change your preferences at any time.',
77      notice: 'Your preferences are stored for {DAYS} days. After that, we will ask again. You can change them anytime via the 🍪 icon in the bottom-left corner.',
78      cookiesLabel: 'cookies', cookieLabel: 'cookie', close: 'Close', reopenTitle: 'Configure cookies'
79    }
80  };
81
82  var T = DEFAULTS[CONFIG.lang] || DEFAULTS.es;
83  if (override.copy) { for (var k in override.copy) { T[k] = override.copy[k]; } }
84
85  // Default category definitions (from the PRD cookie audit). Edit per site.
86  var CATEGORIES = override.categories || [
87    {
88      key: 'necessary', name: (CONFIG.lang === 'en' ? 'Necessary' : 'Necesarias'),
89      locked: true, def: true,
90      desc: (CONFIG.lang === 'en'
91        ? 'Essential for the site to work. Cannot be disabled.'
92        : 'Imprescindibles para el funcionamiento del sitio. No se pueden desactivar.'),
93      cookies: [
94        { name: 'pll_language', provider: 'Polylang — Idioma' },
95        { name: 'oc_cookie_consent', provider: 'Propio — Preferencias guardadas' }
96      ]
97    },
98    {
99      key: 'analytics', name: (CONFIG.lang === 'en' ? 'Analytics' : 'Analíticas'),
100      locked: false, def: false,
101      desc: (CONFIG.lang === 'en'
102        ? 'Help us understand how visitors use the site.'
103        : 'Nos ayudan a entender cómo usan los visitantes el sitio web.'),
104      cookies: [
105        { name: '_ga', provider: 'Google Analytics — ID de usuario' },
106        { name: '_ga_*', provider: 'Google Analytics — Estado de sesión' },
107        { name: '_ym_uid', provider: 'Yandex Metrika — ID de usuario' },
108        { name: '_ym_d', provider: 'Yandex Metrika — Primera visita' },
109        { name: '_ym_isad', provider: 'Yandex Metrika — Detección ad-blocker' }
110      ]
111    },
112    {
113      key: 'marketing', name: 'Marketing',
114      locked: false, def: false,
115      desc: (CONFIG.lang === 'en'
116        ? 'Allow us to show personalised content and ads.'
117        : 'Permiten mostrarte contenido y anuncios personalizados.'),
118      cookies: [
119        { name: '_fbp', provider: 'Meta Pixel — Conversiones' },
120        { name: 'sib_cuid', provider: 'Brevo — ID de contacto email' },
121        { name: 'ml_*', provider: 'MailerLite — Formularios email' }
122      ]
123    }
124  ];
125
126  /* ------------------------------------------------------------------ *
127   * 2. Google Consent Mode v2 — DEFAULT DENY (must run before gtag.js).
128   *    Tracking scripts are text/plain (blocked) until consent, so this
129   *    runs first and Google honours it.
130   * ------------------------------------------------------------------ */
131  window.dataLayer = window.dataLayer || [];
132  function gtag() { window.dataLayer.push(arguments); }
133  window.gtag = window.gtag || gtag;
134  gtag('consent', 'default', {
135    analytics_storage: 'denied',
136    ad_storage: 'denied',
137    ad_user_data: 'denied',
138    ad_personalization: 'denied',
139    wait_for_update: 500
140  });
141
142  function updateConsentMode(state) {
143    gtag('consent', 'update', {
144      analytics_storage:  state.analytics ? 'granted' : 'denied',
145      ad_storage:         state.marketing ? 'granted' : 'denied',
146      ad_user_data:       state.marketing ? 'granted' : 'denied',
147      ad_personalization: state.marketing ? 'granted' : 'denied'
148    });
149  }
150
151  /* ------------------------------------------------------------------ *
152   * 3. Consent storage — cookie (180d) + localStorage mirror
153   * ------------------------------------------------------------------ */
154  function readConsent() {
155    try {
156      var m = document.cookie.match(new RegExp('(?:^|; )' + CONFIG.cookieName + '=([^;]*)'));
157      if (m) {
158        var data = JSON.parse(decodeURIComponent(m[1]));
159        if (data && data.categories) { return data; }
160      }
161    } catch (e) {}
162    try {
163      var ls = localStorage.getItem(CONFIG.storageKey);
164      if (ls) { return JSON.parse(ls); }
165    } catch (e) {}
166    return null;
167  }
168
169  function writeConsent(categories) {
170    var record = {
171      version: CONFIG.version,
172      categories: categories,
173      timestamp: new Date().toISOString(),
174      expiry: CONFIG.expiryDays
175    };
176    var encoded = encodeURIComponent(JSON.stringify(record));
177    var maxAge = CONFIG.expiryDays * 24 * 60 * 60;
178    document.cookie = CONFIG.cookieName + '=' + encoded +
179      '; path=/; max-age=' + maxAge + '; SameSite=Lax' +
180      (location.protocol === 'https:' ? '; Secure' : '');
181    try { localStorage.setItem(CONFIG.storageKey, JSON.stringify(record)); } catch (e) {}
182    return record;
183  }
184
185  /* ------------------------------------------------------------------ *
186   * 4. Apply consent — Consent Mode update + activate tagged scripts
187   *    <script type="text/plain" data-cookie-category="analytics"
188   *            data-src="..."> (or inline content) → real <script>
189   * ------------------------------------------------------------------ */
190  function activateScripts(state) {
191    var blocked = document.querySelectorAll('script[type="text/plain"][data-cookie-category]');
192    for (var i = 0; i < blocked.length; i++) {
193      var node = blocked[i];
194      var cat = node.getAttribute('data-cookie-category');
195      if (cat === 'necessary' || state[cat] !== true) { continue; }
196      if (node.getAttribute('data-oc-activated') === '1') { continue; }
197
198      var s = document.createElement('script');
199      // Copy non-control attributes (id, async, defer, data-* of the vendor, etc.)
200      for (var a = 0; a < node.attributes.length; a++) {
201        var at = node.attributes[a];
202        if (['type', 'data-src', 'data-cookie-category', 'data-oc-activated'].indexOf(at.name) !== -1) { continue; }
203        s.setAttribute(at.name, at.value);
204      }
205      s.type = 'text/javascript';
206      var src = node.getAttribute('data-src');
207      if (src) { s.src = src; } else { s.text = node.textContent || ''; }
208      node.setAttribute('data-oc-activated', '1');
209      node.parentNode.insertBefore(s, node.nextSibling);
210    }
211
212    // Gated iframes / embeds (e.g. Google Maps): data-src -> src on consent.
213    var frames = document.querySelectorAll('iframe[data-cookie-category][data-src]');
214    for (var f = 0; f < frames.length; f++) {
215      var fr = frames[f];
216      var fcat = fr.getAttribute('data-cookie-category');
217      if (fcat === 'necessary' || state[fcat] !== true) { continue; }
218      if (fr.getAttribute('data-oc-activated') === '1') { continue; }
219      fr.src = fr.getAttribute('data-src');
220      fr.setAttribute('data-oc-activated', '1');
221      fr.style.display = '';
222      var ph = fr.previousElementSibling;
223      if (ph && ph.classList && ph.classList.contains('oc-embed-block')) { ph.parentNode.removeChild(ph); }
224    }
225  }
226
227  // Show a placeholder over blocked embeds so there is no empty hole, with a
228  // button that opens the banner to grant consent.
229  function markBlockedEmbeds() {
230    var frames = document.querySelectorAll('iframe[data-cookie-category][data-src]:not([data-oc-activated="1"])');
231    for (var i = 0; i < frames.length; i++) {
232      var fr = frames[i];
233      if (fr.previousElementSibling && fr.previousElementSibling.classList &&
234          fr.previousElementSibling.classList.contains('oc-embed-block')) { continue; }
235      fr.style.display = 'none';
236      var box = document.createElement('div');
237      box.className = 'oc-embed-block';
238      var h = fr.getAttribute('height');
239      if (h && /^\d+$/.test(h)) { box.style.minHeight = h + 'px'; }
240      box.innerHTML = '<div class="oc-embed-inner"><p>' +
241        esc(T.embedBlocked || 'Este contenido se carga desde un servicio externo y requiere tu consentimiento.') +
242        '</p><button type="button" class="oc-btn oc-btn-accept" data-oc="config">' +
243        esc(T.embedButton || 'Configurar y cargar') + '</button></div>';
244      fr.parentNode.insertBefore(box, fr);
245      box.addEventListener('click', function (e) {
246        if (e.target.closest('[data-oc="config"]')) { openPrefs(); }
247      });
248    }
249  }
250
251  function applyConsent(state, persist) {
252    updateConsentMode(state);
253    activateScripts(state);
254    if (persist) {
255      var record = writeConsent(state);
256      logConsentServerSide(record);
257    }
258  }
259
260  /* Optional server-side proof (RGPD Art. 7.1). No-op if no endpoint set. */
261  function logConsentServerSide(record) {
262    if (!CONFIG.logEndpoint) { return; }
263    try {
264      var payload = JSON.stringify({
265        domain: location.hostname,
266        categories: record.categories,
267        user_agent: navigator.userAgent,
268        timestamp: record.timestamp
269      });
270      if (navigator.sendBeacon) {
271        navigator.sendBeacon(CONFIG.logEndpoint, new Blob([payload], { type: 'application/json' }));
272      } else {
273        fetch(CONFIG.logEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, keepalive: true });
274      }
275    } catch (e) {}
276  }
277
278  /* ------------------------------------------------------------------ *
279   * 5. Choice handlers
280   * ------------------------------------------------------------------ */
281  function allCategories(value) {
282    var out = {};
283    CATEGORIES.forEach(function (c) { out[c.key] = c.locked ? true : value; });
284    return out;
285  }
286
287  function acceptAll() { applyConsent(allCategories(true), true); hideBanner(); closePrefs(); }
288  function rejectAll() { applyConsent(allCategories(false), true); hideBanner(); closePrefs(); }
289
290  function savePrefs() {
291    var state = {};
292    CATEGORIES.forEach(function (c) {
293      if (c.locked) { state[c.key] = true; return; }
294      var input = document.getElementById('oc-cat-' + c.key);
295      state[c.key] = !!(input && input.checked);
296    });
297    applyConsent(state, true);
298    hideBanner();
299    closePrefs();
300  }
301
302  /* ------------------------------------------------------------------ *
303   * 6. UI — inject CSS + DOM (mirrors the validated mockup)
304   * ------------------------------------------------------------------ */
305  var ACCENT = CONFIG.accent;
306  var ACCENT_DARK = shade(ACCENT, -0.12);
307
308  function shade(hex, pct) {
309    try {
310      var n = parseInt(hex.replace('#', ''), 16);
311      var r = Math.max(0, Math.min(255, (n >> 16) + Math.round(255 * pct)));
312      var g = Math.max(0, Math.min(255, ((n >> 8) & 0xff) + Math.round(255 * pct)));
313      var b = Math.max(0, Math.min(255, (n & 0xff) + Math.round(255 * pct)));
314      return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
315    } catch (e) { return hex; }
316  }
317
318  var CSS = [
319    '.oc-cookie-banner{position:fixed;bottom:0;left:0;right:0;z-index:99999;background:#fff;border-top:1px solid #e5e0db;box-shadow:0 -4px 30px rgba(0,0,0,.08);padding:20px 32px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;transform:translateY(0);transition:transform .4s cubic-bezier(.16,1,.3,1)}',
320    '.oc-cookie-banner.oc-hidden{transform:translateY(100%)}',
321    '.oc-cookie-inner{max-width:1100px;margin:0 auto;display:flex;align-items:center;gap:28px}',
322    '.oc-cookie-text{flex:1}',
323    '.oc-cookie-text h3{font-size:15px;font-weight:600;color:#1a1a1a;margin:0 0 4px}',
324    '.oc-cookie-text p{font-size:13px;line-height:1.5;color:#666;margin:0}',
325    '.oc-cookie-text a{color:' + ACCENT + ';text-decoration:underline}',
326    '.oc-cookie-actions{display:flex;gap:10px;flex-shrink:0;align-items:center}',
327    '.oc-btn{padding:11px 22px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;border:none;transition:all .2s;white-space:nowrap;text-align:center;font-family:inherit}',
328    '.oc-btn:hover{transform:translateY(-1px)}',
329    '.oc-btn-accept{background:' + ACCENT + ';color:#fff}.oc-btn-accept:hover{background:' + ACCENT_DARK + '}',
330    '.oc-btn-reject{background:#4a4a4a;color:#fff}.oc-btn-reject:hover{background:#333}',
331    '.oc-btn-save{background:' + ACCENT + ';color:#fff;opacity:.7}.oc-btn-save:hover{opacity:1}',
332    '.oc-btn-config{background:transparent;color:' + ACCENT + ';padding:11px 14px;text-decoration:underline;font-weight:500}',
333    '.oc-cookie-prefs{position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.4);backdrop-filter:blur(2px);opacity:0;pointer-events:none;transition:opacity .3s}',
334    '.oc-cookie-prefs.oc-visible{opacity:1;pointer-events:auto}',
335    '.oc-prefs-card{background:#fff;border-radius:16px;width:520px;max-width:95vw;max-height:85vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.2);transform:translateY(20px);transition:transform .3s cubic-bezier(.16,1,.3,1);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}',
336    '.oc-cookie-prefs.oc-visible .oc-prefs-card{transform:translateY(0)}',
337    '.oc-prefs-header{padding:24px 28px 16px;border-bottom:1px solid #f0ebe6;display:flex;justify-content:space-between;align-items:flex-start}',
338    '.oc-prefs-header h2{font-size:18px;font-weight:700;color:#1a1a1a;margin:0 0 4px}',
339    '.oc-prefs-header p{font-size:13px;color:#888;margin:0}',
340    '.oc-prefs-close{width:32px;height:32px;border:none;background:#f5f0eb;border-radius:8px;cursor:pointer;font-size:16px;color:#999;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-left:12px;transition:background .15s}',
341    '.oc-prefs-close:hover{background:#ebe4dc;color:#666}',
342    '.oc-prefs-body{padding:8px 28px 16px}',
343    '.oc-cat{border-bottom:1px solid #f5f0eb;padding:16px 0}.oc-cat:last-child{border-bottom:none}',
344    '.oc-cat-header{display:flex;align-items:center;justify-content:space-between;cursor:pointer}',
345    '.oc-cat-info{flex:1}',
346    '.oc-cat-info h4{font-size:14px;font-weight:600;
346color:#1a1a1a;display:flex;align-items:center;gap:8px;margin:0}',
347    '.oc-cat-info h4 .oc-count{font-size:11px;font-weight:500;color:#999;background:#f5f0eb;padding:1px 7px;border-radius:10px}',
348    '.oc-cat-info p{font-size:12px;color:#888;margin:3px 0 0;line-height:1.4}',
349    '.oc-toggle{position:relative;width:44px;height:24px;flex-shrink:0;margin-left:16px}',
350    '.oc-toggle input{opacity:0;width:0;height:0;position:absolute}',
351    '.oc-toggle .oc-slider{position:absolute;inset:0;background:#ddd;border-radius:24px;cursor:pointer;transition:background .2s}',
352    '.oc-toggle .oc-slider::before{content:"";position:absolute;width:18px;height:18px;background:#fff;border-radius:50%;top:3px;left:3px;transition:transform .2s;box-shadow:0 1px 3px rgba(0,0,0,.15)}',
353    '.oc-toggle input:checked + .oc-slider{background:' + ACCENT + '}',
354    '.oc-toggle input:checked + .oc-slider::before{transform:translateX(20px)}',
355    '.oc-toggle input:disabled + .oc-slider{background:#c4b5a3;cursor:not-allowed}',
356    '.oc-cat-detail{max-height:0;overflow:hidden;transition:max-height .3s ease}',
357    '.oc-cat.oc-expanded .oc-cat-detail{max-height:400px}',
358    '.oc-cat-header .oc-arrow{font-size:10px;color:#bbb;margin-right:8px;transition:transform .2s}',
359    '.oc-cat.oc-expanded .oc-cat-header .oc-arrow{transform:rotate(90deg)}',
360    '.oc-cookie-list{margin-top:10px;background:#faf8f5;border-radius:8px;padding:10px 14px}',
361    '.oc-cookie-item{display:flex;justify-content:space-between;padding:5px 0;font-size:12px;color:#666;border-bottom:1px solid #f0ebe6;gap:12px}',
362    '.oc-cookie-item:last-child{border-bottom:none}',
363    '.oc-cookie-item .oc-name{font-weight:600;color:#555;font-family:monospace;font-size:11px}',
364    '.oc-cookie-item .oc-provider{color:#999;text-align:right}',
365    '.oc-prefs-notice{padding:0 28px 16px;font-size:11px;color:#aaa;line-height:1.4}',
366    '.oc-prefs-footer{padding:16px 28px 24px;display:flex;gap:10px;border-top:1px solid #f0ebe6}',
367    '.oc-prefs-footer .oc-btn{flex:1}',
368    '.oc-cookie-reopen{position:fixed;bottom:20px;left:20px;z-index:99990;width:40px;height:40px;border-radius:50%;background:#fff;border:1px solid #e5e0db;box-shadow:0 2px 12px rgba(0,0,0,.1);cursor:pointer;display:none;align-items:center;justify-content:center;font-size:18px;transition:transform .2s}',
369    '.oc-cookie-reopen:hover{transform:scale(1.1)}.oc-cookie-reopen.oc-visible{display:flex}',
370    '@media(max-width:640px){.oc-cookie-inner{flex-direction:column;gap:14px;text-align:center}.oc-cookie-actions{width:100%;flex-direction:column}.oc-btn{width:100%}.oc-prefs-card{border-radius:16px 16px 0 0}}'
371  ].join('');
372
373  var el = {};
374
375  function build(stored) {
376    var style = document.createElement('style');
377    style.id = 'oc-cookie-style';
378    style.textContent = CSS;
379    document.head.appendChild(style);
380
381    var root = document.createElement('div');
382    root.id = 'oc-cookie-root';
383
384    // Banner
385    var pos = CONFIG.position === 'bottom-left' ? 'left:20px;right:auto;max-width:520px;' : '';
386    root.innerHTML =
387      '<div class="oc-cookie-banner oc-hidden" id="oc-banner" style="' + pos + '">' +
388        '<div class="oc-cookie-inner">' +
389          '<div class="oc-cookie-text">' +
390            '<h3>' + esc(T.bannerTitle) + '</h3>' +
391            '<p>' + esc(T.bannerText) + ' ' +
392              '<a href="' + esc(CONFIG.privacyUrl) + '" target="_blank" rel="noopener">' + esc(T.policyLink) + '</a>' +
393            '</p>' +
394          '</div>' +
395          '<div class="oc-cookie-actions">' +
396            '<button class="oc-btn oc-btn-reject" data-oc="reject">' + esc(T.reject) + '</button>' +
397            '<button class="oc-btn oc-btn-accept" data-oc="accept">' + esc(T.accept) + '</button>' +
398            '<button class="oc-btn oc-btn-config" data-oc="config">' + esc(T.config) + '</button>' +
399          '</div>' +
400        '</div>' +
401      '</div>' +
402      '<div class="oc-cookie-prefs" id="oc-prefs">' +
403        '<div class="oc-prefs-card" role="dialog" aria-modal="true" aria-label="' + esc(T.prefsTitle) + '">' +
404          '<div class="oc-prefs-header">' +
405            '<div><h2>' + esc(T.prefsTitle) + '</h2><p>' + esc(T.prefsSubtitle) + '</p></div>' +
406            '<button class="oc-prefs-close" data-oc="close" title="' + esc(T.close) + '">&times;</button>' +
407          '</div>' +
408          '<div class="oc-prefs-body">' + categoriesHtml(stored) + '</div>' +
409          '<div class="oc-prefs-notice">' + esc(T.notice.replace('{DAYS}', CONFIG.expiryDays)) + '</div>' +
410          '<div class="oc-prefs-footer">' +
411            '<button class="oc-btn oc-btn-reject" data-oc="reject">' + esc(T.reject) + '</button>' +
412            '<button class="oc-btn oc-btn-save" data-oc="save">' + esc(T.save) + '</button>' +
413            '<button class="oc-btn oc-btn-accept" data-oc="accept">' + esc(T.accept) + '</button>' +
414          '</div>' +
415        '</div>' +
416      '</div>' +
417      '<button class="oc-cookie-reopen" id="oc-reopen" title="' + esc(T.reopenTitle) + '">🍪</button>';
418
419    document.body.appendChild(root);
420
421    el.banner = document.getElementById('oc-banner');
422    el.prefs = document.getElementById('oc-prefs');
423    el.reopen = document.getElementById('oc-reopen');
424
425    wireEvents();
426  }
427
428  function categoriesHtml(stored) {
429    return CATEGORIES.map(function (c) {
430      var count = c.cookies.length + ' ' + (c.cookies.length === 1 ? T.cookieLabel : T.cookiesLabel);
431      var checked = c.locked ? true : (stored && stored.categories ? !!stored.categories[c.key] : c.def);
432      var input = '<input type="checkbox" id="oc-cat-' + c.key + '"' +
433        (checked ? ' checked' : '') + (c.locked ? ' disabled' : '') + '>';
434      var rows = c.cookies.map(function (ck) {
435        return '<div class="oc-cookie-item"><span class="oc-name">' + esc(ck.name) +
436          '</span><span class="oc-provider">' + esc(ck.provider) + '</span></div>';
437      }).join('');
438      return '<div class="oc-cat" data-oc-cat>' +
439        '<div class="oc-cat-header" data-oc="expand">' +
440          '<span class="oc-arrow">&#9654;</span>' +
441          '<div class="oc-cat-info"><h4>' + esc(c.name) +
442            ' <span class="oc-count">' + count + '</span></h4><p>' + esc(c.desc) + '</p></div>' +
443          '<label class="oc-toggle" data-oc-stop>' + input + '<span class="oc-slider"></span></label>' +
444        '</div>' +
445        '<div class="oc-cat-detail"><div class="oc-cookie-list">' + rows + '</div></div>' +
446      '</div>';
447    }).join('');
448  }
449
450  function wireEvents() {
451    el.banner.addEventListener('click', dispatch);
452    el.prefs.addEventListener('click', function (e) {
453      if (e.target === el.prefs) { closePrefs(); return; }
454      dispatch(e);
455    });
456    el.reopen.addEventListener('click', function () { showBanner(); });
457    document.addEventListener('keydown', function (e) {
458      if (e.key === 'Escape' && el.prefs.classList.contains('oc-visible')) { closePrefs(); }
459    });
460  }
461
462  function dispatch(e) {
463    var stop = e.target.closest('[data-oc-stop]');
464    var expand = e.target.closest('[data-oc="expand"]');
465    var btn = e.target.closest('[data-oc]');
466    if (stop) { e.stopPropagation(); return; }            // toggle click — don't expand
467    if (expand) {
468      expand.parentNode.classList.toggle('oc-expanded');
469      return;
470    }
471    if (!btn) { return; }
472    switch (btn.getAttribute('data-oc')) {
473      case 'accept': acceptAll(); break;
474      case 'reject': rejectAll(); break;
475      case 'save': savePrefs(); break;
476      case 'config': openPrefs(); break;
477      case 'close': closePrefs(); break;
478    }
479  }
480
481  function showBanner() { el.banner.classList.remove('oc-hidden'); el.reopen.classList.remove('oc-visible'); closePrefs(); }
482  function hideBanner() { el.banner.classList.add('oc-hidden'); el.reopen.classList.add('oc-visible'); }
483  function openPrefs() { syncToggles(); hideBanner(); el.prefs.classList.add('oc-visible'); el.reopen.classList.remove('oc-visible'); }
484  function closePrefs() { el.prefs.classList.remove('oc-visible'); }
485
486  // Sync the panel toggles with the currently stored consent (the panel DOM is
487  // built once, so a prior accept/reject must be reflected when reopening it).
488  function syncToggles() {
489    var c = readConsent();
490    CATEGORIES.forEach(function (cat) {
491      var input = document.getElementById('oc-cat-' + cat.key);
492      if (!input) { return; }
493      if (cat.locked) { input.checked = true; return; }
494      input.checked = (c && c.categories) ? !!c.categories[cat.key] : cat.def;
495    });
496  }
497
498  function esc(s) {
499    return String(s).replace(/[&<>"']/g, function (c) {
500      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
501    });
502  }
503
504  /* ------------------------------------------------------------------ *
505   * 7. Init
506   * ------------------------------------------------------------------ */
507  function init() {
508    var stored = readConsent();
509
510    // Re-apply a valid, non-expired prior consent on every page load.
511    if (stored && stored.categories) {
512      var ts = Date.parse(stored.timestamp);
513      var ageDays = isNaN(ts) ? Infinity : (Date.now() - ts) / 86400000;
514      if (ageDays <= (stored.expiry || CONFIG.expiryDays)) {
515        applyConsent(stored.categories, false);
516        build(stored);
517        el.reopen.classList.add('oc-visible');   // banner stays hidden, 🍪 available
518        return;
519      }
520    }
521
522    // No valid consent → show banner (default deny already set).
523    build(stored);
524    showBanner();
525  }
526
527  if (document.readyState === 'loading') {
528    document.addEventListener('DOMContentLoaded', init);
529  } else {
530    init();
531  }
532
533  // Public API (debug / programmatic reopen)
534  window.ocCookieConsent = {
535    reopen: function () { showBanner(); },
536    get: readConsent,
537    acceptAll: acceptAll,
538    rejectAll: rejectAll
539  };
540})();

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.